只要所有外部函数的变量对象都存在,那么从内部函数引用外部数据则没有特别之处——我们只要遍历作用域链表,查找所需变量。然而,如上文所提及,当一个上下文终止之后,其状态与自身将会被 销毁(destroyed) ,同时内部函数将会从外部函数中返回。此外,这个返回的函数之后可能会在其他的上下文中被激活,那么如果一个之前被终止的含有一些自由变量的上下文又被激活将会怎样?通常来说,解决这个问题的概念在ECMAScript中与作用域链直接相关,被称为 (词法)闭包((lexical) closure)。
ECMAScript中,函数是第一类对象,意味着函数可以作为参数传递给另一个函数,这个被传递的函数一般称为funargs(functional arguments),接收的函数被称为higher-order functions(高阶函数),还有一类返回函数的函数被称为function valued函数。
闭包
一般意义上(与下面理论上相比)闭包产生的条件:
1. 函数可以被作为返回值返回到上级函数或者可以被作为参数传给下级函数;
2. 该语言使用静态作用域,普通函数在执行时使用的是该函数在创建时的作用域链。
function foo() {
var x = 10;
return function bar() {
console.log(x);
};
}
// "foo" returns also a function
// and this returned function uses
// free variable "x"
var returnedFunction = foo();
// global variable "x"
var x = 20;
// execution of the returned function
returnedFunction(); // 10, but not 20
作为返回值的函数在创建时,自身的作用域链中会保存父函数的作用域链。如上面的例子:bar的作用域链中包含foo的作用域链,而bar的作用域链用于在之后调用时查询变量。
// global "x"
var x = 10;
// global function
function foo() {
console.log(x);
}
(function (funArg) {
// local "x"
var x = 20;
// there is no ambiguity,
// because we use global "x",
// which was statically saved in
// [[Scope]] of the "foo" function,
// but not the "x" of the caller's scope,
// which activates the "funArg"
funArg(); // 10, but not 20
})(foo); // pass "down" foo as a "funarg"
funArg在调用时使用的是foo创建时的作用域链,所以x是10。
综上,闭包可以定义为:一个保存了所有父作用域的代码块,该代码块在执行时在这些父作用域中查询变量。
由于ECMAScript中所有的函数在创建时都保存了作用域链,所以理论上说所有的函数都是闭包。
闭包中的数据共享:
几个函数可能拥有相同的父级作用域,这种情况下变量是共享的,一个闭包中的变量改变,另外的也同样改变。
function baz() {
var x = 1;
return {
foo: function foo() { return ++x; },
bar: function bar() { return --x; }
};
}
var closures = baz();
console.log(
closures.foo(), // 2
closures.bar() // 1
);
这就引发了前面一章(使用闭包和立即执行的方法来保存状态,http://blog.youkuaiyun.com/ymjring/article/details/40889257)中提到的问题:
var data = [];
for (var k = 0; k < 3; k++) {
data[k] = function () {
alert(k);
};
}
data[0](); // 3, but not 0
data[1](); // 3, but not 1
data[2](); // 3, but not 2
解决方式:
var data = [];
for (var k = 0; k < 3; k++) {
data[k] = (function (x) {
return function () {
alert(x);
};
})(k); // 将k当做参数传递进去
}
// 结果正确
data[0](); // 0
data[1](); // 1
data[2](); // 2