在对变量对的了解中,变量对象作为执行上下文的数据(包括变量、函数声明、函数形参)的概念已经清楚。 那作用域有一个什么东西?
简洁明了地表现作用域的重点,它主要与内部函数相关。 我们知道,ECMAScript 允许创建内部函数,我们甚至能从父函数中返回这些函数。
var x = 10;
function foo() {
var y = 20;
function bar() {
alert(x + y);
}
return bar;
}
foo()(); // 30
很明显每个上下文拥有自己的变量对象:对于全局上下文,它是全局对象自身;对于函数,它是活动对象。
作用域链正是内部上下文所有变量对象(包括父变量对象)的列表。此链用来变量查询。即在上面的例子中,“bar”上下文的作用域链包括AO(bar)、AO(foo)和VO(global)。
作用域链与一个执行上下文相关,变量对象的链用于在标识符解析中变量查找。函数上下文的作用域链在函数调用时创建的,包含活动对象和这个函数内部的[[scope]]属性.
在上下文中,它的所有变量如下:
activeExecutionContext = {
VO: {...}, // or AO
this: thisValue,
Scope: [ // Scope chain
// 所有变量对象的列表
// for identifiers lookup
]
};
其中scope如下
Scope = AO + [[Scope]]
在进入上下文时函数声明会放到变量/活动对象中。
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
];
进入创建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
全局上下文的变量对象是:
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”上下文的作用域链为: ```javascript 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
在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]]持续存在,即使是在函数创建的作用域已经完成之后。
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
全局上下文的作用域链仅包含全局对象。代码eval的上下文与当前的调用上下文(calling context)拥有同样的作用域链。
globalContext.Scope = [
Global
];
evalContext.Scope === callingContext.Scope;
感谢@汤姆大叔 的《深入理解JavaScript系列》指导我学习!