闭包
在我们讨论闭包之前,最好先来回顾以下Javascript中作用域的概念,然后再进行某些话题拓展。
作用域链
尽管在Javascript中不存在大括号级的作用域,但它有函数作用域,也就是说,在某函数内定义的所有变量在该函数外是不可见的。但如果该变量是在某代码块中被定义的(如在某个if或for语句中),那它在代码块外是可见的。
var a = 1;
function f() {
var b = 1;
return a;
}
f(); // 1
b; // b is not defined
在这里,变量a是属于全局域的,而变量b的作用域就在函数f()内了。所以:
- 在f()内,a和b都是可见的;
- 在f()外,a是可见的,b则是不可见的。
在下面的例子中,如果我们在函数outer()
定义了另一个函数inner()
,那么,在inner()中可以访问的变量即来自它自己的作用域,也可以来则其“父级”作用域。这就形成了一条作用域链,该链的长度(或深度)则取决于我们的需要。
var global = 1;
function outer() {
var outer_local = 2;
function inner () {
var inner_local = 3;
return inner_local + outer_local +global;
}
return inner();
}
outer(); // 6
利用闭包突破作用域
现在,让我们想通过图示的方式来介绍以下闭包的概念。让我们通过这段代码了解其中的奥秘。
var a = "global variable";
var F = function () {
var b = "local variable";
var N = function () {
var c = "inner variable"
}
}
首先当然就是全局作用域G,我们可以将其视为包含一切的宇宙。
其中包含各种全局变量(a1,a2),和函数(如F)。
每个函数也都会拥有一块自己的私有空间,用以储存一些别的变量(例如b)以内部函数(例如N)。所以,我们最终可以把示意图化成这样。
再上图中,如果我们在a点,那么就位于全局空间中。而如果是在b点我们就在函数F的空间里,在这里我们即可以访问全局空间,也可以访问F空间。如果我们在c点,那就位于函数N中,我们可以访问的空间包括全局空间,F空间和N空间。其中,a和b之间是不联通的,因为b在F以外是不可见的。但是如果愿意的话,我们是可以将c点和b点联通起来,或者说将N于b联通起来。当我们将N的空间拓展到F以外,并止步于全局空间以内时,就产生了一件有趣的东西——闭包。
知道接下来会发生什么吗?N将会和a一样置身全局空间。而且由于由于函数还记得它在被定义时所设定的环境,因此它依然可以访问F空间,并使用b。这很有趣,因为现在N和a处于同一空间,但是N可以访问b,而a不能。
那么,N究竟时如何突破作用域的呢?我们只需要将它升级为全局变量(不使用var语句)或通过F传递给(或返回)给全局空间即可。下面,我们来看看具体时怎么实现的。
闭包#1
首先我们来看一个函数。这个函数与之前所描述的一样,之不过在F中多了返回N,而在函数N中多了返回变量b,N和b都可以通过作用域链进行访问。
var a = "global variable";
var F = function () {
var b = "local variable";
var N = function () {
var c = "inner local";
return b;
};
return N;
}
函数F中包含了局部变量b,因此后者在全局空间里是不可见的。
b; // b is not defined
函数N有自己的私有空间,同时也可以访问F()的空间和全局空间 ,所以b对它来说是可见的。因为F()是可以在全局空间中被调用的(它是一个全局函数),所以我们可以将它的返回值赋值给另一个全局变量,从而生成一个可以访问F()私有空间的新全局函数。
var inner = F();
inner(); // "local variable"
闭包#2
下面这个例子的最终结果与之前相同,但是实现方法上存在一些细微的不同。在这里F()不再返回函数了,而是直接在函数体内创建一个新的全局函数inner()。
首相,我们需要声明一个全局函数的占位符。尽管这种占位符不是必须的,但是还是声明一下,然后,我们就可以将函数F()定义如下:
var inner; // 占位符
var F = function () {
var b = "local variable";
var N = function () {
return b;
};
inner = N;
};
F()被调用时会发生什么:
F();
我们在F()中定义了一个新的函数N(),并且将它赋值给了全局变量inner。由于N()是在F()内部定义的,它可以访问F()的作用域,所以即使该函数后来审计成了全局函数,但它依然可以保留对F()作用域的访问权。
inner();
"local variable"
相关定义的闭包#3
事实上,每个函数都可以被认为是一个闭包。因为每个函数都在其所在域(即该函数的作用域)中维护了某种私有联系。但是大多数时候,该作用域在函数执行完之后就自行销毁了——除非发生一些有趣的事(比如像上一小节所述的那样)。导致作用域被保持。
根据目前的讨论,我们可以说,如果一个函数会在其父级函数返回之后留住对父级作用域的链接的话(如上例,F是N的父级函数,F返回之后,N依然可以访问F中的局部变量),相关的闭包就会被创建起来。但其实每个函数本身就是一个闭包,因为每个函数至少都会有访问全局作用域的权限,而全局作用域是不会被破坏的。
让我们再来看一个闭包的例子。这次我们使用的是函数参数。该参数与函数的局部变量没有什么不同,但是它们是隐式创建的(即它们不需要使用var来声明。)我们在这里创建一个函数,该函数返回一个子函数,而这个子函数返回的则是其父函数的参数。
function F(param) {
var N = function () {
return param;
}
param++;
return N;
}
然后我们可以这样调用它:
var inner = F(123);
inner(); // 124
请注意,当我们的返回函数被调用时(N被赋值时函数并没有被调用,调用是在N被求值,也就是执行return N
语句时发生的),param++已经执行过一次递增操作了。所以inner()返回的是更新后的值。由于,该函数所绑定的是作用域本身,而不是在函数定义时该作用域中的变量或变量当前所返回的值。
循环中的闭包
接下来,让我们来看看新手们在闭包问题上会犯哪些典型的错误。毕竟有闭包所导致的bug往往很难被发现。因为它们总是看起来一切正常。
让我来看一个三次的循环操作,它在每次迭代中都会创建一个返回当前循环序号的函数。该新函数会被添加到一个数组中,并最终返回。具体如下:
function F() {
var arr = [], i;
for (i = 0; i < 3; i++) {
arr[i] = function () {
return i;
};
}
return arr;
}
//下面我们来运行一下函数,并将解惑复制给数组arr;
var arr = F();
现在我们拥有了一个包含三个函数的数组。你可以通过在每个数组元素后面加一对括号来调用它们。按通常的估计,它们英爱会依照循环顺序分别输出0,1,2,下面我们来运行:
arr[0] (); // 3
arr[1] () // 3
arr[2](); // 3
显然这并不是我们想要的结果。究竟是怎么回事呢?原来我们在这里创建了3个闭包,而它们都指向一个共同的局部变量i。但是闭包并不会i记录它们的值,它们所拥有的知识相关域在创建时的一个链接(即引用)。在这个例子中,变量i恰巧存在于定义这三个函数域中。对这三个函数中的任何一个而言,当它要去获取某个变量时,它会从其所在的域开始逐级寻找那个距离最近的i值。由于循环结束时i的值为3,所以这3个函数指向了一个共同的值。
为什么结果是3,而不是2呢?这也是一个值的思考的问题,它能帮助你更好的理解for循环,请自行思考:
function F() {
var arr = [], i;
for (i = 0; i < 3; i++){
arr[i] = (function (x) {
return function () {
return x;
}
} (i) );
}
return arr;
}
这样就能获得我们所要的结果了:
var arr = F();
arr[0] (); // 0
arr[1] (); // 1
arr[2] (); // 2
在这里,我们不再直接创建一个返回i的函数了,而是将i传递给另一个即时函数。在该函数中,i就被赋值给了局部变量x,这样一来,每次迭代中的x就会拥有各自不同的值了。
或者,我们可以定义一个”正常点的“内部函数(不使用即时函数)来实现相同的功能。要点是在每次迭代操作中,我们要在中间函数内将i的值”本地化“。
function F() {
function binder(x) {
return function() {
return x;
};
}
var arr = [], i;
for (i = ; i < 3; i++){
arr[i] = binder(i);
}
return arr;
}