这篇翻译紧接上一篇,建议先阅读JavaScript作用域链中的标识符解析和闭包

从我之前的文章中,我们知道每个函数调用时都有一个关联的执行上下文,其中包含一个变量对象[VO],它由给定函数内部定义的所有变量,函数和参数组成。

每个执行上下文的作用域链属性只是当前上下文的[VO] +所有父级的[VO]的集合

1
2
Scope = VO + All Parent VOs
Eg: scopeChain = [ [VO] + [VO1] + [VO2] + [VO n+1] ];

确定作用域链中的变量对象[VO]

我们现在知道作用域链的第一个[VO]属于当前执行上下文,我们可以通过查看他的父上下文的作用域链来找到剩余的父[VO]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function one() {

two();

function two() {

three();

function three() {
alert('I am at function three');
}

}

}

one();

这个例子很简单,从全局上下文开始,我们调用one(),one()调用two(),然后调用three(),three函数弹出一个alert。上面的图像显示了执行上下文堆栈的情况。我们可以看到此时的three作用域链如下所示:
three() Scope Chain = [ [three() VO] + [two() VO] + [one() VO] + [Global VO] ];

词法作用域

要注意的JavaScript的一个重要特性是,解释器使用词法作用域,而不是动态作用域。对所有内部函数来说,他们静态的绑定到父上下文中,即他们在代码中被定义的地方(个人理解为javascript的函数作用域在函数定义时确定,而非调用时)。

在上面的上一个例子中,three将始终静态绑定到two,而two依次绑定到one,依此类推。这给出了链接效果,其中所有内部函数都可以通过静态绑定的作用域链访问外部函数VO。

这个词法作用域是许多开发人员混淆的根源。我们知道每次调用函数都会创建一个新的执行上下文和相关的VO,它保存在当前上下文中计算的变量值。

正是这种对VO的动态的,运行时的评估以及每个上下文的词法作用域,导致程序行为出现意外结果。采用以下经典示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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的值,所以aleert将分别警告1,2,3,4和5。

这是最常见的混淆点。函数inner是在全局上下文中创建的,因此其作用域链静态地绑定到全局上下文。

第11~15行调用inner(),它将在inner.ScopeChain中查找解析i,而inner函数定义在全局上下文中。当for循环结束时,i已经增加到5,每次调用inner时都会得到相同的结果。静态绑定的作用域链,含有来自包含实时变量的每个上下文的[VOs],通常会让开发人员感到意外。

确定变量的值

以下示例alert变量a,b和c的和,它给出了6的结果。

第14行很有趣,乍一看似乎a和b不在函数three内,所以这段代码怎么还能运行?要理解解释器如何评估此代码,我们需要在执行第14行时查看函数three的作用域链:

当解释器执行第14行:alert(a + b + c)时,它通过查看作用域链并检查第一个变量对象,即[three( ) VO]。它会检查[three( ) VO]中是否存在a,但是找不到具有该名称的任何属性,因此继续检查下一个[VO]。

解释器按顺序检查每个[VO]是否存在变量名,如果存在值将返回到原始计算代码,否则程序将抛出ReferenceError。因此,给定上面的示例,您可以看到在函数three的作用域链中,a,b和c都是可解析的。

闭包是怎么工作的

在JavaScript中,闭包通常被认为是某种魔法,只有高级开发人员才能真正理解,但事实上,它只是对作用域链的简单理解。正如Douglas Crockford所说,闭包只是:

An inner function always has access to the vars and parameters of its outer function, even after the outer function has returned…

内部函数始终能够访问外部函数的变量和参数,即使外部函数已经被返回

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

1
2
3
4
5
6
7
8
9
10
function foo() {
var a = 'private variable';
return function bar() {
alert(a);
}
}

var callAlert = foo();

callAlert(); // private variable

全局上下文有一个名为foo的函数和一个名为callAlert的变量,它保存foo( )返回的值。开发人员常常感到惊讶和困惑的是,即使在foo完成执行后,foo的私有变量a仍然可以被callAlert访问。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Global Context when evaluated
global.VO = {
foo: pointer to foo(),
callAlert: returned value of global.VO.foo
scopeChain: [global.VO]
}

// Foo Context when evaluated
foo.VO = {
bar: pointer to bar(),
a: 'private variable',
scopeChain: [foo.VO, global.VO]
}

// Bar Context when evaluated
bar.VO = {
scopeChain: [bar.VO, foo.VO, global.VO]
}

现在我们可以通过调用callAlert来看到,我们得到函数foo( )返回值,它返回指向bar函数的指针。在执行bar函数时,bar.VO.scopeChain是[bar.VO,foo.VO,global.VO]。

alert(a),解释器检查bar.VO.scopeChain中的第一个VO,查找名为a的属性但找不到匹配项,因此立即转到下一个VO,foo.VO。

它检查属性的存在,这次找到一个匹配,将值返回到bar上下文,这解释了为什么即使foo已经完成执行,还是能访问’私有变量’a。

到此为止,我们已经涵盖了作用域链及其词法作用域的细节,以及闭包和变量解析是如何工作。本文的其余部分将讨论与上述内容相关的一些有趣情况。

原型链是如何影响变量的?

JavaScript本质上是基于原型链的,除了null和undefined之外,语言中的几乎所有内容都是对象。当试图访问对象上的属性时,解释器将尝试通过查找对象中的属性。如果它找不到属性,它将继续查找原型链,这是一个继承的对象链,直到它找到属性,或遍历到链的末尾。

这导致了一个有趣的问题,解释器是先使用作用链还是原型链解析对象属性?两者都会使用。尝试解析属性或标识符时,将首先使用作用链来寻找对象。找到对象后,将遍历该对象的原型链,查找属性名称。我们来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
var bar = {};

function foo() {

bar.a = 'Set from foo()'

return function inner() {
alert(bar.a);
}

}

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

第5行在全局对象bar上创建属性a,并将其值设置为‘Set from foo()’。解释器查找作用域链,并按预期在全局上下文中查找bar.a。现在,让我们考虑以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
var bar = {};

function foo() {

Object.prototype.a = 'Set from prototype';

return function inner() {
alert(bar.a);
}

}

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

在运行时,我们调用inner函数,它试图通过在其作用域链中查找bar的存在来解析bar.a。它在全局上下文中找到bar,并进入搜索bar以查找名为a的属性。但是,a从未在bar上设置,因此解释器遍历bar对象的原型链并发现在Object.prototype上设置了一个。

这种行为解释了标识符的解析:在作用链中找到对象,然后继续向上找对象的原型链,直到找到属性,或返回undefined。

什么时候使用闭包

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

封装
允许我们在暴露受控公共接口的同时,从外部作用域隐藏上下文的实现细节。这通常被称为模块模式或揭示模式。

回调
也许闭包最强大的用途之一就是回调。浏览器中的JavaScript通常在单线程事件循环中运行,阻止其他事件直到当前事件执行完成。回调允许我们以非阻塞方式推迟对函数的调用,通常是为了响应事件的完成。这方面的一个例子是在对服务器进行AJAX调用时,使用回调来处理响应,同时仍然保持创建它的绑定。

闭包作为参数
我们还可以将闭包作为参数传递给函数,这是一个强大的函数范例,用于为复杂代码创建更优雅的解决方案。以最小排序函数为例,通过将闭包作为参数传递,我们可以为不同类型的数据排序定义实现,同时仍然重用单个函数体作为原理图。

什么时候不使用闭包

虽然闭包很强大,但由于某些性能问题,应该谨慎使用它们:

长作用域链
多个嵌套函数是您可能遇到某些性能问题的典型信号。请记住,每次需要确定变量值时,必须遍历作用链以查找标识符,因此不言而喻,作用域链越长,查找时间越长。

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

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

循环引用
不太喜欢原文的内容,后续找个好的例子

原文地址

传送:http://davidshariff.com/blog/javascript-scope-chain-and-closures/