深入理解JavaScript系列(16) 闭包(Closures)(3)


var x = 10;

function foo() {
alert(x);
}

// foo是闭包
foo: <FunctionObject> = {
[[Call]]: <code block of foo>,
[[Scope]]: [
global: {
x: 10
}
],
... // 其它属性
};


如我们所说,为了优化目的,当一个函数没有使用自由变量的话,实现可能不保存在副作用域链里。不过,在ECMA-262-3规范里任何都没说。因此,正常来说,所有的参数都是在创建阶段保存在[[Scope]]属性里的。

有些实现中,允许对闭包作用域直接进行访问。比如Rhino,针对函数的[[Scope]]属性,对应有一个非标准的 __parent__属性,在第12章中作过介绍:

复制代码 代码如下:


var global = this;
var x = 10;

var foo = (function () {

var y = 20;

return function () {
alert(y);
};

})();

foo(); // 20
alert(foo.__parent__.y); // 20

foo.__parent__.y = 30;
foo(); // 30

// 可以通过作用域链移动到顶部
alert(foo.__parent__.__parent__ === global); // true
alert(foo.__parent__.__parent__.x); // 10


所有对象都引用一个[[Scope]]
这里还要注意的是:在ECMAScript中,同一个父上下文中创建的闭包是共用一个[[Scope]]属性的。也就是说,某个闭包对其中[[Scope]]的变量做修改会影响到其他闭包对其变量的读取:

这就是说:所有的内部函数都共享同一个父作用域

复制代码 代码如下:


var firstClosure;
var secondClosure;

function foo() {

var x = 1;

firstClosure = function () { return ++x; };
secondClosure = function () { return --x; };

x = 2; // 影响 AO["x"], 在2个闭包公有的[[Scope]]中

alert(firstClosure()); // 3, 通过第一个闭包的[[Scope]]
}

foo();

alert(firstClosure()); // 4
alert(secondClosure()); // 3


关于这个功能有一个非常普遍的错误认识,开发人员在循环语句里创建函数(内部进行计数)的时候经常得不到预期的结果,而期望是每个函数都有自己的值。

复制代码 代码如下:


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]]中创建额外的变量对象。要注意的是,在返回的函数中,如果要获取“k”的值,那么该值还是会是3。

顺便提下,大量介绍JavaScript的文章都认为只有额外创建的函数才是闭包,这种说法是错误的。实践得出,这种方式是最有效的,然而,从理论角度来说,在ECMAScript中所有的函数都是闭包。

然而,上述提到的方法并不是唯一的方法。通过其他方式也可以获得正确的“k”的值,如下所示:

复制代码 代码如下:

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wdjzfz.html