对JavaScript闭包的理解

对JS闭包的理解

每当我们声明一个函数,这个函数就会有闭包域,这个闭包域也就是我们常说的闭包。

大家都知道的两点:
1.闭包域内声明的变量和方法,外部无法访问到。
2.闭包域可以访问到外部的变量和方法。

这很好理解,子函数的生命周期依附于父函数,父函数内的变量(从全局看是私有变量),对于子函数来讲是全局的。

当在一个闭包域中又包含另一个闭包域(也就是在函数内部声明另一个函数),子闭包域即当前的子函数对象的function scopes会产生一个closure对象属性,在这个对象中包含子闭包域对父闭包域的所有引用。

先来一发代码:

1
2
3
4
5
6
7
8
9
10
var foo = function () {
var i = 0;
for(i = 0; i < 6; i ++) {
function foo2 () {
console.log(i);
}
}
foo2();
};
foo();

foo2是子函数,接下来我们看一下Chrome Debugger的内容:

先插一句:function scope不是对象,不是属性,我对他的理解是JavaScript引擎对函数的一种参数引用机制。

可以清楚地看到,foo2的function scopes内有Closure和Global两个对象属性,foo2这个函数对象所引用的i值为6。

倘若在子闭包域仍旧存活的状态下(当然此时父函数依然在栈中),父函数的私有变量其值出现变化。那么,对这个私有变量进行引用的子闭包域内的closure对象内的引用值也会发生变化,因为其实质是引用

接下来,举一个经典的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function foo () {
var result = [];
for(var i = 0; i < 3; i ++) {
result.push(function() {
return i * i;
});
}
return result;
}
var results = foo();//results:[function,function...function];
var ele1 = results[0];
var ele2 = results[1];
var ele3 = results[2];
/*分别调用*/
ele1();//9
ele2();//9
ele3();//9

运行后程序会如注释所展示的那样,每一个ele的结果都是一样的,也就是i最后的值3的平方。只要知道实质是引用这一点,也就很好理解为什么会出现这样的结果了。

当调用了foo()的时候,foo内部的私有变量result数组内的每一个元素都被push进了一个需要对另一个私有变量i进行引用的function,这些匿名函数的function scope内的closure都会有对当前i的引用,函数体内的i的值取决于函数调用时的i的值。


现在我们来试着令其正常输出为0,1,4.

第一种思路:通过函数传参立即执行以保存状态。

1
2
3
4
5
6
7
8
9
10
function foo() {
var result = [];
for(var i = 0; i < 3; i ++) {
result.push((function(i) {
return function () {
return i * i;
})(i));
}
return result;
}

第二种思路:通过Function构造器,以字符串拼接的形式保存状态。

1
2
3
4
5
6
function foo() {
var result = [];
for(var i = 0; i < 3; i ++) {
result.push(new Function("return " i + "*" + i + ";");)
}
}

在ES6中,新增了let关键字,使得变量获得了块级作用域。

1
2
3
4
5
6
7
function foo() {
var result = [];
for(let i = 0; i < 3; i ++) {
result.push(function () {return i * i});
}
return result;
}