理解闭包

MDN的解释:

闭包是指那些能够访问独立(自由)变量的函数 (变量在本地使用,但定义在一个封闭的作用域中)。换句话说,这些函数可以“记忆”它被创建时候的环境。

创建闭包的通常方式,是在一个函数内部创建另一个函数。
由于作用域链的结构,外围函数是无法访问内部变量的,为了能够访问内部变量,我们就可以使用闭包,闭包的本质还是函数。

理解闭包,必须弄清执行环境,作用域这些概念,上篇文章有总结。
例:

1
2
3
4
5
6
7
8
function A(){
var x = 1;
return function(){
console.log(x);
}
}
var m = A();
m(); //1

难点一: 判断作用域指向的变量对象是否相同

1
2
3
4
5
6
7
8
9
10
11
12
13
function A(){
var x = 1;
return function(){
x++;
console.log(x);
}
}
var m1 = A(); //第一次执行A函数
m1(); // 2
m1(); // 3
var m2 = A(); //第二次执行A函数
m2();// 2
m1();// 4

图例

每次执行A函数时,都会生成一个A的活动变量和执行环境,执行完毕以后,A的执行环境销毁,但是活动对象由于被闭包函数引用,所以仍然保留,最终剩下两个A的变量对象,因此m1和m2在操作x时,指向的是不同的数据。

理清了这个过程,下面三个问题就很容易回答了:

问题1:为什么连续执行m1的时候,x的值在递增?
回答:因为m1在引用的活动对象A一直没有释放(想释放的话可以让m1=null),所以x的值一直递增。

问题2:定义函数m2的时候,为什么x的值重新从1开始了?
回答:因为又一次运行了A函数,生成一个新的A的活动对象,所以m2的作用域链引用的是一个新的x值。

问题3: m1和m2里面的x为什么是相互独立,各自维持的?
回答:因为在定义m1和m2的时候,分别运行了A函数,生成了两个活动对象,所以,m1和m2的作用域链是指向不同的A的活动对象的。

难点二: 判断变量对象中变量的值

下面是常见的列子:

1
2
3
4
5
6
7
8
9
10
11
12
function A(){
var funs = [];
for(var i=0; i<10; i++){
funs[i] = function(){
return i;
}
}
return funs;
}
var fn = A(); // 定义funs[0]-funs[9],10个函数
console.log(fn[0]()); // 10
console.log(fn[1]()); // 10

解释:
当执行 var funs = A();时,只是定义,而没执行,产生环境变量的是在console.log(funs[0]());此时A的变量对象中i值已经是10,自然返回的都是10。

解决办法1:用ES6let 代替 var
解决办法2:用立即执行函数:

1
2
3
4
5
6
7
8
9
10
11
function A(){
var funs = [];
for(var i = 0; i < 10; i++){
funs[i] = function tmp1(n){ // tmp1可忽略不写
return function tmp2(){ // tmp2可忽略不写
return n;
}
}(i)
}
return funs;
}

tmp1是立即执行函数。定义之后立即执行,即把i的值立即传递给n,然后再返回tmp2,此时执行10次,生成了10个tmp2的活动变量。每个tmp2变量对象保存的就是循环i的值。