原文链接:ECMA-262-3 in detail. Chapter 6. Closures.
=============================================================
ECMA的闭包实现
var x = 10;
function foo() {
console.log(x);
}
(function (funArg) {
var x = 20;
// variable "x" for funArg is saved statically
// from the (lexical) context, in which it was created
// therefore:
funArg(); // 10, but not 20
})(foo);
ECMA用的是静态作用域,不是动态作用域。
不论函数有没有被真正执行过,它的作用域链都会在创建时被保存。
有些引擎可能会优化没有被访问到的自由变量(free variable)。
有些引擎实现了__parent__,允许直接访问作用域,但是Chrome和Firefox都没有。
=============================================================
一[[Scope]]永逸
在同一个上下文里创建的不同闭包(就是内函数对象),它们保存的变量是指向同一个对象的,通过某个内函数闭包对其进行的修改会影响到所有其余的闭包。
var firstClosure;
var secondClosure;
function foo() {
var x = 1;
firstClosure = function () { return ++x; };
secondClosure = function () { return --x; };
x = 2; // affection on AO["x"], which is in [[Scope]] of both closures
console.log(firstClosure()); // 3, via firstClosure.[[Scope]]
}
foo();
console.log(firstClosure()); // 4
console.log(secondClosure()); // 3
演示这个问题的一个经典面试题是:
var data = [];
for (var k = 0; k < 3; k++) {
data[k] = function () {
console.log(k);
};
}
data[0](); // 3, but not 0
data[1](); // 3, but not 1
data[2](); // 3, but not 2
这是由于闭包共享和变量提升导致的一个常见陷阱!
上述代码创建的闭包的情况是:
activeContext.Scope = [
... // higher variable objects
{data: [...], k: 3} // activation object
];
data[0].[[Scope]] === Scope;
data[1].[[Scope]] === Scope;
data[2].[[Scope]] === Scope;
解决办法是通过函数表达式给每次循环里创建一个新作用域,并在新作用域里用一个新变量保存每次循环里k的值,这些新作用域被独立保存在每个数组元素的函数的闭包里,互不影响。
如下:
var data = [];
for (var k = 0; k < 3; k++) {
data[k] = (function _helper(x) {
return function () {
console.log(x);
};
})(k); // pass "k" value
}
// now it is correct
data[0](); // 0
data[1](); // 1
data[2](); // 2
它创建的闭包的情况是:
data[0].[[Scope]] === [
... // higher variable objects
AO of the parent context: {data: [...], k: 3},
AO of the _helper context: {x: 0}
];
data[1].[[Scope]] === [
... // higher variable objects
AO of the parent context: {data: [...], k: 3},
AO of the _helper context: {x: 1}
];
data[2].[[Scope]] === [
... // higher variable objects
AO of the parent context: {data: [...], k: 3},
AO of the _helper context: {x: 2}
];
作者也介绍了另外一个方案:
var data = [];
for (var k = 0; k < 3; k++) {
(data[k] = function () {
console.log(arguments.callee.x);
}).x = k; // save "k" as a property of the function
}
// also everything is correct
data[0](); // 0
data[1](); // 1
data[2](); // 2
疑问:我印象中有一本书建议不要使用arguments.callee,所以这个方案或许不值得推荐。
另外,ES6里面的let关键字解决了这个问题,在《YDtKJS:Scope & Closure》里也交代了。
=============================================================
函数参数和return
其实我没有太看懂这一节。
作者讨论的是从语义上,return的行为。似乎作者的意思是在ECMA里,return的职责很简单,就仅仅是返回控制流程到呼叫者。
function getElement() {
[1, 2, 3].forEach(function (element) {
if (element % 2 == 0) {
// return to "forEach" function,
// but not return from the getElement
console.log('found: ' + element); // found: 2
return element;
}
});
return null;
}
console.log(getElement()); // null, but not 2
并且与用try-catch的情况做了比对:
var $break = {};
function getElement() {
try {
[1, 2, 3].forEach(function (element) {
if (element % 2 == 0) {
// "return" from the getElement
console.log('found: ' + element); // found: 2
$break.data = element;
throw $break;
}
});
} catch (e) {
if (e == $break) {
return $break.data;
}
}
return null;
}
console.log(getElement()); // 2
=============================================================
理论版本(theory versions)
作者重新归纳了跟闭包的定义有关的内容。重新定性地描述何为闭包?
实践上来讲,被作为返回值被返回的函数是闭包,访问了自由变量(free variable)的函数是闭包。
=============================================================
闭包的实际用途
- 应用于数组,用来遍历数组元素的函数,比如sort(),map(),forEach()。
- apply与call,它们是受启发于函数编程里面的函数参数。
(function () { console.log([].join.call(arguments, ';')); // 1;2;3 }).apply(this, [1, 2, 3]);
- 延时调用,比如setTimeout()。
- 还有回调函数,比如Ajax里面的回调方法。
最常见的用法当然还是用来实现封装:
var foo = {};
// initialization
(function (object) {
var x = 10;
object.getX = function _getX() {
return x;
};
})(foo);
console.log(foo.getX()); // get closured "x" – 10