# 闭包
函数即是数据。就比方说,函数可以赋值给变量,可以当参数传递给其他函数,还可以从函数里返回等等。这类函数有特殊的名字和结构
函数式参数(“Funarg”): 是指值为函数的参数
例子:
function exampleFunc(funArg) {
funArg();
}
exampleFunc(function () {
alert('funArg');
});
高阶函数(high-order function 简称:HOF): 指接受函数式参数的函数。 上述例子中 exampleFunc
就是这样的函数
带函数值的函数: 以函数作为返回值的函数
自由变量
自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量
function testFn() {
var localVar = 10;
function innerFn(innerParam) {
alert(innerParam + localVar);
}
return innerFn;
}
var someFn = testFn();
someFn(20); // 30
上述例子中,对于 innerFn
函数来说,localVar
就属于自由变量
# 什么是闭包
闭包是代码块和创建该代码块的上下文中数据的结合
让我们来看下面这个例子(伪代码):
var x = 20;
function foo() {
alert(x); // 自由变量"x" == 20
}
// 伪代码 foo闭包
fooClosure = {
call: foo // 引用到function
lexicalEnvironment: {x: 20} // 搜索上下文的上下文
};
上述例子中,fooClosure
部分是伪代码。对应的,在ECMAScript中,foo
函数已经有了一个内部属性——创建该函数上下文的作用域链。
lexical
通常是省略的。上述例子中是为了强调在闭包创建的同时,上下文的数据就会保存起来。当下次调用该函数的时候,自由变量就可以在保存的(闭包)上下文中找到了,正如上述代码所示,变量 “z”
的值总是 10
# ECMAScript闭包的实现
首先回顾一下 ECMAScript 的静态(词法)作用域
var z = 10;
function foo() {
alert(z);
}
foo(); // 10 – 使用静态和动态作用域的时候
(function () {
var z = 20;
foo(); // 10 – 使用静态作用域, 20 – 使用动态作用域
})();
// 将foo作为参数的时候是一样的
(function (funArg) {
var z = 30;
funArg(); // 10 – 静态作用域, 30 – 动态作用域
})(foo);
静态(词法)作用域意味着作用域在函数创建时创建的,且在之后的代码运行过程中不会被改变
回到正题,先来一个例子:
var x = 10;
function foo() {
alert(x);
}
(function (funArg) {
var x = 20;
// 变量"x"在(lexical)上下文中静态保存的,在该函数创建的时候就保存了
funArg(); // 10, 而不是20
})(foo);
技术上说,创建该函数的父级上下文的数据是保存在函数的内部属性 [[Scope]]
中,回到上文说的什么是闭包:闭包是代码块和创建该代码块的上下文中数据的结合,上面的例子上代码是 foo
,创建 foo
的上下文是全全局上下文, 所以换句话说在 ECMAScript 中,所有的函数都是闭包,因为它们都是在创建的时候就保存了上层上下文的作用域链
var x = 10;
function foo() {
alert(x);
}
// foo是闭包
foo: <FunctionObject> = {
[[Call]]: <code block of foo>,
[[Scope]]: [
global: {
x: 10
}
],
... // 其它属性
};
同一个父上下文中创建的闭包是共用一个 [[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
var data = [];
for (var k = 0; k < 3; k++) {
(data[k] = function () {
alert(arguments.callee.x);
}).x = k; // 将k作为函数的一个属性
}
// 结果也是对的
data[0](); // 0
data[1](); // 1
data[2](); // 2
通过其他方式也可以获得正确的 “k”
的值,如下所示:
var data = [];
for (var k = 0; k < 3; k++) {
(data[k] = function () {
alert(arguments.callee.x);
}).x = k; // 将k作为函数的一个属性
}
// 结果也是对的
data[0](); // 0
data[1](); // 1
data[2](); // 2
# 总结
ECMAScript中,闭包指的是:
从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
从实践角度:以下函数才算是闭包:
即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
在代码中引用了自由变量
← 全面解析Module模式 设计模式 →