从上篇文章中,我们知道每个函数都有有个关联的包含VO对象的执行上下文execution context,,它由所给定的本地函数中定义的所有变量,函数和参数组成的。
每一个执行上下文中的 范围链(scope chain)的属性 是当前上下文的VO对象 和所有父级VO对象的集合。
Scope = VO + All Parent VOs
Eg: scopeChain = [ [VO] + [VO1] + [VO2] + [VO n+1] ];
我们现在知道,范围链的第一个【VO】是属于当前的执行上下文,而且我们可以通过查看父级上下文的范围链找到剩余的父级【VO】对象。
function one() { two(); function two() { three(); function three() {
alert('I am at function three');
} }
}
one();
这个例子很明显,一开始,我们从全局上下文中调用 one(),one() 调用 two(),two() 再调 three(),然后在第三个方法中 alert 信息。我们可以看到,scope chain
在这个时间点看起来如下:
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
与每个上下文的词法(静态)定义范围配对,导致程序行为的意外结果。以下列经典例子:
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
包含实时变量的静态范围链通常会让开发者感到惊讶。
以下示例提醒变量的值a
,b
并c
为我们提供了6的结果。
function one() { var a = 1;
two(); function two() { var b = 2;
three(); function three() { var c = 3;
alert(a + b + c); // 6
} }
}
one();
第14行是有趣,乍一看似乎a
并b
没有“内部”功能三,怎么能这个代码仍然可以工作?要了解解释器如何评估此代码,我们需要在执行时间线14时查看函数三的范围链:
当解释器执行第14行:alert(a + b + c)
它a
首先通过查看范围链和检查第一个变量对象来解析three's [VO]
。它检查以查看是否a
存在内部three's [VO]
但找不到具有该名称的任何属性,因此继续检查下一个[VO]
。
解释器[VO]
按顺序检查变量名称的存在,在这种情况下,该值将返回到原始评估代码,否则程序将抛出一个ReferenceError
如果没有找到的值。因此,给定上述示例,您可以看到a
,b
并且c
都是可解析的给定函数三的作用域链。
在JavaScript中,关闭通常被视为某种神奇的独角兽,只有高级开发人员才能真正理解,但是真实的说,这只是对范围链的简单理解。克罗克福德说,封闭是简单的:
内部函数总是可以访问其外部函数的vars和参数,即使在外部函数返回后也是如此
下面的代码是一个关闭的例子:
function foo() {
var a = 'private variable';
return function bar() {
alert(a);
}
}
var callAlert = foo();
callAlert(); // private variable
它global context
有一个命名的函数foo()
和一个名为的变量callAlert
,它保存返回的值foo()
。什么经常令人惊讶和困惑的开发人员是私有变量,a
即使在foo()
执行完毕后仍然可用。
但是,如果我们详细地查看每个上下文,我们将看到如下内容:
// 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 chain
其lexical
环境的细节,以及如何closures
和variable resolution
工作。本文的其余部分将介绍与上述相关的一些有趣的情况。
JavaScript是自然的原型,几乎所有的语言,除了null
和undefined
,是objects
。当尝试访问某个属性时object
,解释器将尝试通过查找该属性的存在来解决该属性object
。如果找不到该属性,它将继续查找原型链,这是一个继承的对象链,直到找到该属性,或者遍历到链的末尾。
这导致一个有趣的问题,解释器是否使用scope chain
或prototype chain
第一个解析对象属性?它使用两者。当尝试解析属性或标识符时,scope chain
将首先使用它来定位object
。一旦object
被发现,将prototype chain
那object
将被遍历查找属性名称。我们来看一个例子:
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
。现在,让我们考虑一下:
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
。
正是这种确切的行为解释了标识符的解析; 找到object
的scope chain
,然后进行了对象prototype chain
,直到属性没有被找到,或退回undefined
。
闭包是给JavaScript提供的强大概念,使用它们的一些最常见的情况是:
虽然关闭功能强大,但由于某些性能问题,应该谨慎使用:
JavaScript是一种garbage collected
语言,这意味着与较低级编程语言不同,开发人员通常不必担心内存管理。但是,这种自动垃圾收集通常会导致开发人员的应用程序遭遇性能差和内存泄漏。
不同的JavaScript引擎实现垃圾收集略有不同,因为ECMAScript没有定义如何处理实现,但是当尝试创建高性能,无泄漏的JavaScript代码时,相同的原理可以应用于引擎。一般来说,垃圾收集器在程序中运行的任何其他活动对象都无法引用对象时,会尝试释放对象的内存,或者无法访问。
这导致我们关闭,以及在程序中循环引用的可能性,这是用于描述一个对象引用另一个对象的情况的术语,并且该对象指向第一个对象。关闭特别容易受到泄漏的影响,请记住,即使在父执行完成并返回之后,内部函数也可以引用范围链中进一步定义的变量。大多数JavaScript引擎都很好地处理这些情况(你是IE),但在进行开发时仍然值得注意并考虑。
对于旧版本的IE,引用DOM元素通常会导致内存泄漏。为什么?在IE中,JavaScript(JScript?)引擎和DOM都有自己的单独的垃圾收集器。所以当引用JavaScript中的DOM元素时,本地收集器将交给DOM并且DOM收集器指向本机,导致收集器都不知道循环引用。
从许多开发商在过去几年的工作,我经常发现的概念scope chain
和closures
被称为一下,但是在细节上没有真正了解。我希望这篇文章有助于让您了解基本概念,更深入地了解基本概念。
展望未来,您应该掌握所有您需要的知识,以确定在任何情况下变量的解析如何在编写JavaScript时起作用。快乐编码!