在第12章关于变量对象的描述中,咱们已经知道一个执行上下文 的数据(变量、函数声明和函数的形参)做为属性存储在变量对象中。前端
同时咱们也知道变量对象在每次进入上下文时建立,并填入初始值,值的更新出如今代码执行阶段。算法
这一章专门讨论与执行上下文直接相关的更多细节,此次咱们将说起一个议题——做用域链。数组
英文原文:http://dmitrysoshnikov.com/ecmascript/chapter-4-scope-chain/
中文参考:http://www.denisdeng.com/?p=908
本文绝大部份内容来自上述地址,仅作少量修改,感谢做者
若是要简要的描述并展现其重点,那么做用域链大多数与内部函数相关。闭包
咱们知道,ECMAScript 容许建立内部函数,咱们甚至能从父函数中返回这些函数。ecmascript
var x = 10;
function foo() {
var y = 20;
function bar() {
alert(x + y);
}
return bar;
}
foo()(); // 30
这样,很明显每一个上下文拥有本身的变量对象:对于全局上下文,它是全局对象自身;对于函数,它是活动对象。ide
做用域链正是内部上下文全部变量对象(包括父变量对象)的列表。此链用来变量查询。即在上面的例子中,“bar”上下文的做用域链包括AO(bar)、AO(foo)和VO(global)。函数
可是,让咱们仔细研究这个问题。this
让咱们从定义开始,并进深一步的讨论示例。prototype
做用域链与一个执行上下文相关,变量对象的链用于在标识符解析中变量查找。
函数上下文的做用域链在函数调用时建立的,包含活动对象和这个函数内部的[[scope]]属性。下面咱们将更详细的讨论一个函数的[[scope]]属性。code
在上下文中示意以下:
activeExecutionContext = {
VO: {...}, // or AO
this: thisValue,
Scope: [ // Scope chain
// 全部变量对象的列表
// for identifiers lookup
]
};
其scope定义以下:
Scope = AO + [[Scope]]
这种联合和标识符解析过程,咱们将在下面讨论,这与函数的生命周期相关。
函数的的生命周期分为建立和激活阶段(调用时),让咱们详细研究它。
众所周知,在进入上下文时函数声明放到变量/活动(VO/AO)对象中。让咱们看看在全局上下文中的变量和函数声明(这里变量对象是全局对象自身,咱们还记得,是吧?)
var x = 10;
function foo() {
var y = 20;
alert(x + y);
}
foo(); // 30
在函数激活时,咱们获得正确的(预期的)结果--30。可是,有一个很重要的特色。
此前,咱们仅仅谈到有关当前上下文的变量对象。这里,咱们看到变量“y”在函数“foo”中定义(意味着它在foo上下文的AO中),可是变量“x”并未在“foo”上下文中定义,相应地,它也不会添加到“foo”的AO中。乍一看,变量“x”相对于函数“foo”根本就不存在;但正如咱们在下面看到的——也仅仅是“一瞥”,咱们发现,“foo”上下文的活动对象中仅包含一个属性--“y”。
fooContext.AO = {
y: undefined // undefined – 进入上下文的时候是20 – at activation
};
函数“foo”如何访问到变量“x”?理论上函数应该能访问一个更高一层上下文的变量对象。实际上它正是这样,这种机制是经过函数内部的[[scope]]属性来实现的。
[[scope]]是全部父变量对象的层级链,处于当前函数上下文之上,在函数建立时存于其中。
注意这重要的一点--[[scope]]在函数建立时被存储--静态(不变的),永远永远,直至函数销毁。即:函数能够永不调用,但[[scope]]属性已经写入,并存储在函数对象中。
另一个须要考虑的是--与做用域链对比,[[scope]]是函数的一个属性而不是上下文。考虑到上面的例子,函数“foo”的[[scope]]以下:
foo.[[Scope]] = [
globalContext.VO // === Global
];
举例来讲,咱们用一般的ECMAScript 数组展示做用域和[[scope]]。
继续,咱们知道在函数调用时进入上下文,这时候活动对象被建立,this和做用域(做用域链)被肯定。让咱们详细考虑这一时刻。
正如在定义中说到的,进入上下文建立AO/VO以后,上下文的Scope属性(变量查找的一个做用域链)做以下定义:
Scope = AO|VO + [[Scope]]
上面代码的意思是:活动对象是做用域数组的第一个对象,即添加到做用域的前端。
Scope = [AO].concat([[Scope]]);
这个特色对于标示符解析的处理来讲很重要。
标示符解析是一个处理过程,用来肯定一个变量(或函数声明)属于哪一个变量对象。
这个算法的返回值中,咱们总有一个引用类型,它的base组件是相应的变量对象(或若未找到则为null),属性名组件是向上查找的标示符的名称。引用类型的详细信息在第13章.this中已讨论。
标识符解析过程包含与变量名对应属性的查找,即做用域中变量对象的连续查找,从最深的上下文开始,绕过做用域链直到最上层。
这样一来,在向上查找中,一个上下文中的局部变量较之于父做用域的变量拥有较高的优先级。万一两个变量有相同的名称但来自不一样的做用域,那么第一个被发现的是在最深做用域中。
咱们用一个稍微复杂的例子描述上面讲到的这些。
var x = 10;
function foo() {
var y = 20;
function bar() {
var z = 30;
alert(x + y + z);
}
bar();
}
foo(); // 60
对此,咱们有以下的变量/活动对象,函数的的[[scope]]属性以及上下文的做用域链:
全局上下文的变量对象是:
globalContext.VO === Global = {
x: 10
foo: <reference to function>
};
在“foo”建立时,“foo”的[[scope]]属性是:
foo.[[Scope]] = [
globalContext.VO
];
在“foo”激活时(进入上下文),“foo”上下文的活动对象是:
fooContext.AO = {
y: 20,
bar: <reference to function>
};
“foo”上下文的做用域链为:
fooContext.Scope = fooContext.AO + foo.[[Scope]] // i.e.:
fooContext.Scope = [
fooContext.AO,
globalContext.VO
];
内部函数“bar”建立时,其[[scope]]为:
bar.[[Scope]] = [
fooContext.AO,
globalContext.VO
];
在“bar”激活时,“bar”上下文的活动对象为:
barContext.AO = {
z: 30
};
“bar”上下文的做用域链为:
barContext.Scope = barContext.AO + bar.[[Scope]] // i.e.:
barContext.Scope = [
barContext.AO,
fooContext.AO,
globalContext.VO
];
对“x”、“y”、“z”的标识符解析以下:
- "x"
-- barContext.AO // not found
-- fooContext.AO // not found
-- globalContext.VO // found - 10
- "y"
-- barContext.AO // not found
-- fooContext.AO // found - 20
- "z"
-- barContext.AO // found - 30
让咱们看看与做用域链和函数[[scope]]属性相关的一些重要特征。
在ECMAScript中,闭包与函数的[[scope]]直接相关,正如咱们提到的那样,[[scope]]在函数建立时被存储,与函数共存亡。实际上,闭包是函数代码和其[[scope]]的结合。所以,做为其对象之一,[[Scope]]包括在函数内建立的词法做用域(父变量对象)。当函数进一步激活时,在变量对象的这个词法链(静态的存储于建立时)中,来自较高做用域的变量将被搜寻。
例如:
var x = 10;
function foo() {
alert(x);
}
(function () {
var x = 20;
foo(); // 10, but not 20
})();
咱们再次看到,在标识符解析过程当中,使用函数建立时定义的词法做用域--变量解析为10,而不是30。此外,这个例子也清晰的代表,一个函数(这个例子中为从函数“foo”返回的匿名函数)的[[scope]]持续存在,即便是在函数建立的做用域已经完成以后。
关于ECMAScript中闭包的理论和其执行机制的更多细节,阅读16章闭包。
在上面的例子中,咱们看到,在函数建立时得到函数的[[scope]]属性,经过该属性访问到全部父上下文的变量。可是,这个规则有一个重要的例外,它涉及到经过函数构造函数建立的函数。
var x = 10;
function foo() {
var y = 20;
function barFD() { // 函数声明
alert(x);
alert(y);
}
var barFE = function () { // 函数表达式
alert(x);
alert(y);
};
var barFn = Function('alert(x); alert(y);');
barFD(); // 10, 20
barFE(); // 10, 20
barFn(); // 10, "y" is not defined
}
foo();
咱们看到,经过函数构造函数(Function constructor)建立的函数“bar”,是不能访问变量“y”的。但这并不意味着函数“barFn”没有[[scope]]属性(不然它不能访问到变量“x”)。问题在于经过函构造函数建立的函数的[[scope]]属性老是惟一的全局对象。考虑到这一点,如经过这种函数建立除全局以外的最上层的上下文闭包是不可能的。
在做用域链中查找最重要的一点是变量对象的属性(若是有的话)须考虑其中--源于ECMAScript 的原型特性。若是一个属性在对象中没有直接找到,查询将在原型链中继续。即常说的二维链查找。(1)做用域链环节;(2)每一个做用域链--深刻到原型链环节。若是在Object.prototype 中定义了属性,咱们能看到这种效果。
function foo() {
alert(x);
}
Object.prototype.x = 10;
foo(); // 10
活动对象没有原型,咱们能够在下面的例子中看到:
function foo() {
var x = 20;
function bar() {
alert(x);
}
bar();
}
Object.prototype.x = 10;
foo(); // 20
若是函数“bar”上下文的激活对象有一个原型,那么“x”将在Object.prototype 中被解析,由于它在AO中不被直接解析。但在上面的第一个例子中,在标识符解析中,咱们到达全局对象(在一些执行中并不全是这样),它从Object.prototype继承而来,响应地,“x”解析为10。
一样的状况出如今一些版本的SpiderMokey 的命名函数表达式(缩写为NFE)中,在那里特定的对象存储从Object.prototype继承而来的函数表达式的可选名称,在Blackberry中的一些版本中,执行时激活对象从Object.prototype继承。可是,关于该特点的更多细节在第15章函数讨论。
这里不必定颇有趣,但必需要提示一下。全局上下文的做用域链仅包含全局对象。代码eval的上下文与当前的调用上下文(calling context)拥有一样的做用域链。
globalContext.Scope = [
Global
];
evalContext.Scope === callingContext.Scope;
在ECMAScript 中,在代码执行阶段有两个声明能修改做用域链。这就是with声明和catch语句。它们添加到做用域链的最前端,对象须在这些声明中出现的标识符中查找。若是发生其中的一个,做用域链简要的做以下修改:
Scope = withObject|catchObject + AO|VO + [[Scope]]
在这个例子中添加对象,对象是它的参数(这样,没有前缀,这个对象的属性变得能够访问)。
var foo = {x: 10, y: 20};
with (foo) {
alert(x); // 10
alert(y); // 20
}
做用域链修改为这样:
Scope = foo + AO|VO + [[Scope]]
咱们再次看到,经过with语句,对象中标识符的解析添加到做用域链的最前端:
var x = 10, y = 10;
with ({x: 20}) {
var x = 30, y = 30;
alert(x); // 30
alert(y); // 30
}
alert(x); // 10
alert(y); // 30
在进入上下文时发生了什么?标识符“x”和“y”已被添加到变量对象中。此外,在代码运行阶段做以下修改:
一样,catch语句的异常参数变得能够访问,它建立了只有一个属性的新对象--异常参数名。图示看起来像这样:
try {
...
} catch (ex) {
alert(ex);
}
做用域链修改成:
var catchObject = {
ex: <exception object>
};
Scope = catchObject + AO|VO + [[Scope]]
在catch语句完成运行以后,做用域链恢复到之前的状态。
在这个阶段,咱们几乎考虑了与执行上下文相关的全部经常使用概念,以及与它们相关的细节。按照计划--函数对象的详细分析:函数类型(函数声明,函数表达式)和闭包。顺便说一下,在这篇文章中,闭包直接与[[scope]]属性相关,可是,关于它将在合适的篇章中讨论。我很乐意在评论中回答你的问题。