前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JavaScript范围链中的标识符解析和闭包

JavaScript范围链中的标识符解析和闭包

作者头像
用户7293182
发布2022-01-20 14:09:13
9640
发布2022-01-20 14:09:13
举报
文章被收录于专栏:jQuery每日经典

从上篇文章中,我们知道每个函数都有有个关联的包含VO对象的执行上下文execution context,,它由所给定的本地函数中定义的所有变量,函数和参数组成的。

每一个执行上下文中的 范围链(scope chain)的属性 是当前上下文的VO对象 和所有父级VO对象的集合。

代码语言:javascript
复制
Scope = VO + All Parent VOs
Eg: scopeChain = [ [VO] + [VO1] + [VO2] + [VO n+1] ];

确定一个范围链中的Variable Objects [VO]s

我们现在知道,范围链的第一个【VO】是属于当前的执行上下文,而且我们可以通过查看父级上下文的范围链找到剩余的父级【VO】对象。

代码语言:javascript
复制
function one() {   two();   function two() {       three();       function three() {
           alert('I am at function three');
       }   }
}
one();

这个例子很明显,一开始,我们从全局上下文中调用 one(),one() 调用 two(),two() 再调 three(),然后在第三个方法中 alert 信息。我们可以看到,scope chain在这个时间点看起来如下:

代码语言:javascript
复制
three() Scope Chain = [ [three() VO] + [two() VO] + [one() VO] + [Global VO] ];

词汇范围

JavaScript要注意的一个重要特征是解释器Lexical Scoping与动态范围相反。这只是一个复杂的方式来表达所有内部函数,它们是静态的(词法上的)绑定到内部函数在程序代码中被物理定义的父上下文。

在上面的例子中,调用内部函数的顺序并不重要。three()将永远是静态的two(),反过来也将永远束缚one()等等。这给出了链接效果,其中所有内部函数可以VO通过静态绑定访问外部函数Scope Chain

lexical scope是许多开发人员的混乱的根源。我们知道,函数的每次调用都会创建一个新的execution context和关联的VO,它保存当前上下文中评估的变量的值。

这是动态的运行时评估,VO与每个上下文的词法(静态)定义范围配对,导致程序行为的意外结果。以下列经典例子:

代码语言:javascript
复制
var myAlerts = [];
for (var i = 0; i < 5; i++) {
   myAlerts.push(
       function inner() {
           alert(i);
       }
   );
}

myAlerts[0](); // 5
myAlerts[1](); // 5
myAlerts[2](); // 5
myAlerts[3](); // 5
myAlerts[4](); // 5

乍看之下,那些新的JavaScript将被假定alert(i);i在源代码中物理定义函数的每个增量的值,分别提示1,2,3,4和5。

这是最常见的困惑点。函数inner是在全局环境中创建的,因此其范围链是静态绑定到全局上下文。

行11〜15调用inner(),它看起来inner.ScopeChain解决i,其位于global上下文。在每次调用时i,已经增加到5,每次调用相同的结果inner()[VOs]每个context包含实时变量的静态范围链通常会让开发者感到惊讶。

解决变量的值

以下示例提醒变量的值abc为我们提供了6的结果。

代码语言:javascript
复制
function one() {   var a = 1;
   two();   function two() {       var b = 2;
       three();       function three() {           var c = 3;
           alert(a + b + c); // 6
       }   }
}
one();

第14行是有趣,乍一看似乎ab没有“内部”功能三,怎么能这个代码仍然可以工作?要了解解释器如何评估此代码,我们需要在执行时间线14时查看函数三的范围链:

当解释器执行第14行:alert(a + b + c)a首先通过查看范围链和检查第一个变量对象来解析three's [VO]。它检查以查看是否a存在内部three's [VO]但找不到具有该名称的任何属性,因此继续检查下一个[VO]

解释器[VO]按顺序检查变量名称的存在,在这种情况下,该值将返回到原始评估代码,否则程序将抛出一个ReferenceError如果没有找到的值。因此,给定上述示例,您可以看到ab并且c都是可解析的给定函数三的作用域链。

这与闭包有什么关系?

在JavaScript中,关闭通常被视为某种神奇的独角兽,只有高级开发人员才能真正理解,但是真实的说,这只是对范围链的简单理解。克罗克福德说,封闭是简单的:

内部函数总是可以访问其外部函数的vars和参数,即使在外部函数返回后也是如此

下面的代码是一个关闭的例子:

代码语言:javascript
复制
function foo() {
   var a = 'private variable';
   return function bar() {
       alert(a);
   }
}

var callAlert = foo();
callAlert(); // private variable

global context有一个命名的函数foo()和一个名为的变量callAlert,它保存返回的值foo()。什么经常令人惊讶和困惑的开发人员是私有变量,a即使在foo()执行完毕后仍然可用。

但是,如果我们详细地查看每个上下文,我们将看到如下内容:

代码语言:javascript
复制
// Global Context when evaluatedglobal.VO = {
   foo: pointer to foo(),
   callAlert: returned value of global.VO.foo
   scopeChain: [global.VO]}// Foo Context when evaluatedfoo.VO = {
   bar: pointer to bar(),
   a: 'private variable',
   scopeChain: [foo.VO, global.VO]}// Bar Context when evaluatedbar.VO = {
   scopeChain: [bar.VO, foo.VO, global.VO]}

现在我们可以通过调用看到callAlert(),我们得到的函数foo()返回指针bar()。在进入bar()bar.VO.scopeChain[bar.VO, foo.VO, global.VO]

通过提醒a,解释检查的第一VO bar.VO.scopeChain的命名属性a,但无法找到匹配,因此迅速移动到下一个VO, foo.VO

它检查属性的存在,这个时候找到了匹配,返回值回bar背景下,这解释了为什么alert给我们'private variable'虽甚至foo()已经完成了前一段时间执行。

在这篇文章中,我们已经介绍了scope chainlexical环境的细节,以及如何closuresvariable resolution工作。本文的其余部分将介绍与上述相关的一些有趣的情况。

等等,原型链如何影响变量分辨率?

JavaScript是自然的原型,几乎所有的语言,除了nullundefined,是objects。当尝试访问某个属性时object,解释器将尝试通过查找该属性的存在来解决该属性object。如果找不到该属性,它将继续查找原型链,这是一个继承的对象链,直到找到该属性,或者遍历到链的末尾。

这导致一个有趣的问题,解释器是否使用scope chainprototype chain第一个解析对象属性?它使用两者。当尝试解析属性或标识符时,scope chain将首先使用它来定位object。一旦object被发现,将prototype chainobject将被遍历查找属性名称。我们来看一个例子:

代码语言:javascript
复制
var bar = {};
function foo() {   bar.a = 'Set from foo()';   return function inner() {
       alert(bar.a);
   }
}
foo()(); // 'Set from foo()'

第5行在a全局对象上创建属性bar,并将其值设置为'Set from foo()'。翻译人员会看到scope chain和正如预期bar.a的一样global context。现在,让我们考虑一下:

代码语言:javascript
复制
var bar = {};

function foo() {   Object.prototype.a = 'Set from prototype';   return function inner() {
       alert(bar.a);
   }
}

foo()(); // 'Set from prototype()'

在运行时,我们调用inner()bar.a通过查看其范围链来尝试解决存在的问题 bar。它 bar 在全局范围内发现,并继续搜索bar名为的属性a。然而,a从来没有设置过bar,所以解释器遍历对象的原型链,并且找到a被设置Object.prototype

正是这种确切的行为解释了标识符的解析; 找到objectscope chain,然后进行了对象prototype chain,直到属性没有被找到,或退回undefined

何时使用闭包?

闭包是给JavaScript提供的强大概念,使用它们的一些最常见的情况是:

  • 封装 允许我们从外部范围隐藏上下文的实现细节,同时暴露受控的公共接口。这通常被称为模块模式或显示模块模式。
  • 回调 也许关闭的最强大的用途之一是回调。浏览器中的JavaScript通常运行在单个线程事件循环中,阻止其他事件启动,直到一个事件完成。回调允许我们以非阻塞的方式延迟函数的调用,通常是响应事件完成。一个例子是当对服务器进行AJAX调用时,使用回调来处理响应,同时仍然保持创建它的绑定。
  • 关闭作为参数 我们还可以将闭包作为参数传递给函数,这是一个功能强大的功能范例,可为复杂代码创建更优雅的解决方案。以例如最小排序函数为例。通过将闭包作为参数,我们可以定义不同类型数据排序的实现,同时仍然将单个函数体作为原理图重用。

何时不使用关闭?

虽然关闭功能强大,但由于某些性能问题,应该谨慎使用:

  • 大范围长度 多个嵌套函数是您可能遇到一些性能问题的典型标志。请记住,每次需要评估一个变量时,必须遍历范围链以找到标识符,所以不言而喻,定义变量的链条越远,查找时间就越长。

垃圾收集

JavaScript是一种garbage collected语言,这意味着与较低级编程语言不同,开发人员通常不必担心内存管理。但是,这种自动垃圾收集通常会导致开发人员的应用程序遭遇性能差和内存泄漏。

不同的JavaScript引擎实现垃圾收集略有不同,因为ECMAScript没有定义如何处理实现,但是当尝试创建高性能,无泄漏的JavaScript代码时,相同的原理可以应用于引擎。一般来说,垃圾收集器在程序中运行的任何其他活动对象都无法引用对象时,会尝试释放对象的内存,或者无法访问。

通函

这导致我们关闭,以及在程序中循环引用的可能性,这是用于描述一个对象引用另一个对象的情况的术语,并且该对象指向第一个对象。关闭特别容易受到泄漏的影响,请记住,即使在父执行完成并返回之后,内部函数也可以引用范围链中进一步定义的变量。大多数JavaScript引擎都很好地处理这些情况(你是IE),但在进行开发时仍然值得注意并考虑。

对于旧版本的IE,引用DOM元素通常会导致内存泄漏。为什么?在IE中,JavaScript(JScript?)引擎和DOM都有自己的单独的垃圾收集器。所以当引用JavaScript中的DOM元素时,本地收集器将交给DOM并且DOM收集器指向本机,导致收集器都不知道循环引用。

概要

从许多开发商在过去几年的工作,我经常发现的概念scope chainclosures被称为一下,但是在细节上没有真正了解。我希望这篇文章有助于让您了解基本概念,更深入地了解基本概念。

展望未来,您应该掌握所有您需要的知识,以确定在任何情况下变量的解析如何在编写JavaScript时起作用。快乐编码!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2017-08-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 jQuery每日经典 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 确定一个范围链中的Variable Objects [VO]s
  • 词汇范围
  • 解决变量的值
  • 这与闭包有什么关系?
  • 等等,原型链如何影响变量分辨率?
  • 何时使用闭包?
  • 何时不使用关闭?
  • 垃圾收集
    • 通函
    • 概要
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档