作用域链、变量对象、闭包
变量对象:
1.变量对象(variable object) 是与执行上下文相关的 数据作用域(scope of data) 。
它是与上下文关联的特殊对象,用于存储被定义在上下文中的 变量(variables) 、 函数声明(function declarations) 和 函数的形参 。
2.就像我们所说的, VO就是执行上下文的属性(property):
activeExecutionContext = {
VO: {
// 上下文数据(var, FD, function arguments)
}
};
3.在函数执行上下文中,VO是不能直接访问的,此时由活动对象(activation object,缩写为AO)扮演VO的角色。
4.活动对象是在进入函数上下文时刻被创建的,它通过函数的arguments属性初始化。arguments属性的值是Arguments对象:
AO = {
arguments: <ArgO>
};
****5.执行上下文的代码被分成两个基本的阶段来处理:
- 进入执行上下文:这个阶段AO/VO已经拥有了属性,不过并不是所有的属性都有值,大部分属性的值还是系统默认的初始值undefined
- 执行代码:这个阶段AO/VO属性值被修改
6.通常,各类文章和JavaScript相关的书籍都声称:“不管是使用var关键字(在全局上下文)还是不使用var关键字(在任何地方),都可以声明一个变量”。请记住,这是错误的概念:
任何时候,变量只能通过使用var关键字才能声明。
7.填充VO的优先级顺序是: 函数的形参 -> 函数申明 -> 变量申明。
作用域链:
1.作用域链正是内部上下文所有变量对象(包括父变量对象)的列表。此链用来变量查询。
var x = 10;
function foo() {
var y = 20;
function bar() {
alert(x + y);
}
return bar;
}
foo()(); // 30
上面的例子中,“bar”上下文的作用域链包括AO(bar)、AO(foo)和VO(global)。
2.函数上下文的作用域链在函数调用时创建的,包含活动对象和这个函数内部的[[scope]]属性。下面我们将更详细的讨论一个函数的[[scope]]属性。
在上下文中示意如下:
activeExecutionContext = {
VO: {...}, // or AO
this: thisValue,
Scope: [ // Scope chain
// 所有变量对象的列表
// for identifiers lookup
]
};
其scope定义如下:
Scope = AO + [[Scope]]
<上面代码的意思是:活动对象是作用域数组的第一个对象,即添加到作用域链的前端。>
3.[[scope]]特点:
•[[scope]]是所有父变量对象的层级链,处于当前函数上下文之上,在函数创建时存于其中。
•注意这重要的一点--[[scope]]在函数创建时被存储--静态(不变的),永远永远,直至函数销毁。即:函数可以永不调用,但[[scope]]属性已经写入,并存储在函数对象中。
•另外一个需要考虑的是--与作用域链对比,[[scope]]是函数的一个属性而不是上下文。
4.如果一个属性在对象中没有直接找到,查询将在原型链中继续。即常说的二维链查找。
活动对象没有原型,我们可以在下面的例子中看到:
function foo() {
var x = 20;
function bar() {
alert(x);
}
bar();
}
Object.prototype.x = 10;
foo(); // 20
三、闭包:
1.闭包是代码块和创建该代码块的上下文中数据的结合。
闭包是一系列代码块(在ECMAScript中是函数),并且静态保存所有父级的作用域。
闭包就是能够读取上层上下文内部变量的函数。
2.在ECMAScript中,所有的函数都是闭包,因为它们都是在创建的时候就保存了上层上下文的作用域链(除开异常的情况) (不管这个函数后续是否会激活 —— [[Scope]]在函数创建的时候就有了)
3.在ECMAScript中,同一个父上下文中创建的闭包是共用一个[[Scope]]属性的。也就是说,某个闭包对其中[[Scope]]的变量做修改会影响到其他闭包对其变量的读取
这就是说:所有的内部函数都共享同一个父作用域
var data = [];
for (var k = 0; k < 3; k++) {
data[k] = function () {
alert(k);
};
}
data[0](); // 3, 而不是0
data[1](); // 3, 而不是1
data[2](); // 3, 而不是2
上述例子就证明了 —— 同一个上下文中创建的闭包是共用一个[[Scope]]属性的。因此上层上下文中的变量“k”是可以很容易就被改变的。
activeContext.Scope = [
... // 其它变量对象
{data: [...], k: 3} // 活动对象
];
data[0].[[Scope]] === Scope;
data[1].[[Scope]] === Scope;
data[2].[[Scope]] === Scope;
这样一来,在函数激活的时候,最终使用到的k就已经变成了3了。如下所示,创建一个闭包就可以解决这个问题了:
var data = [];
for (var k = 0; k < 3; k++) {
data[k] = (function _helper(x) {
return function () {
alert(x);
};
})(k); // 传入"k"值
}
// 现在结果是正确的了
data[0](); // 0
data[1](); // 1
data[2](); // 2
函数“_helper”创建出来之后,通过传入参数“k”激活。其返回值也是个函数,该函数保存在对应的数组元素中。这种技术产生了如下效果: 在函数激活时,每次“_helper”都会创建一个新的变量对象,其中含有参数“x”,“x”的值就是传递进来的“k”的值。这样一来,返回的函数的[[Scope]]就成了如下所示:
data[0].[[Scope]] === [
... // 其它变量对象
父级上下文中的活动对象AO: {data: [...], k: 3},
_helper上下文中的活动对象AO: {x: 0}
];
data[1].[[Scope]] === [
... // 其它变量对象
父级上下文中的活动对象AO: {data: [...], k: 3},
_helper上下文中的活动对象AO: {x: 1}
];
data[2].[[Scope]] === [
... // 其它变量对象
父级上下文中的活动对象AO: {data: [...], k: 3},
_helper上下文中的活动对象AO: {x: 2}
];
我们看到,这时函数的[[Scope]]属性就有了真正想要的值了,为了达到这样的目的,我们不得不在[[Scope]]中创建额外的变量对象。