闭包 closure

闭包在网上有各种各样的解释,就像是哈姆雷特一样。

个人偏向 MDN 的解释

A closure is the combination of a function and the lexical environment within which that function was declared.
闭包是函数和声明该函数的词法环境的组合。

我理解的是,闭包描述的是一种现象 phenomenon 。由于 js 存在作用域链的机制,导致一些函数在声明时使用了(n 级)祖先作用域的变量。我强调了声明,是因为看到了 MDN 解释中的这一句

Running this code has exactly the same effect as the previous example of the init() function above; what’s different — and interesting — is that the displayName() inner function is returned from the outer function before being executed.

重点在于:函数在执行前被返回。
下面看一段函数:

1
2
3
4
5
6
7
function sayHello(name) {
return function() {
console.log("hello" + name);
};
}
let greet = sayHello("John");
greet();

这是经典的闭包例子。上面代码中在执行前被返回匿名函数 function anonymous(){console.log("hello" + name)} 与其所在的词法环境(fucntion sayHello)组合成为一个闭包。

同时,也经常看到一个没有利用闭包造成的反例:

1
2
3
4
5
6
7
// 预期结果是1秒后输出 0,1,2,3...9
for (var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// 然而,实际是最后输出了 10 个 10

分析时需要知道的是,for + var 并没有自己的作用域 scope,上面的 for 循环相当于以下写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 循环开始前准备(注意,var是没有块级作用域的)
var i = 0;

// 循环体
if(i < 10){
setTimeout(/*function-console*/, 1000)
}
// 循环体的后操作
i++;

// 下一个循环体及后操作
if(i < 10){
setTimeout(/*function-console*/, 1000)
}
i++;

//再重复几次循环体及后操作

而每次 setTimeout 的时候,第一个入参为一个函数,由于 if 内没有自己的 scope,此函数使用了全局变量 i。声明的时候,function-console 并没有和它所在的环境形成闭包。同时,由于 JS 单线程的机制,function-console 是在 1000ms 后才被推入事件队列中,而此时,全局变量的 i 早已经变为了 10。

如果改为一下使用 let 的写法:

1
2
3
4
5
6
for (let i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// 就能得到预期的输出

这是因为 let 在 for 中形成了一个块级作用域,相当于以下写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
// 循环开始前准备
let i = 0;

// 循环体
if(i < 10){
let temp_i = i;
setTimeout(/*function-console, use temp_i*/, 1000)
}
// 循环体的后操作
i++;

// 下一个循环体及后操作
if(i < 10){
let temp_i = i;
setTimeout(/*function-console, use temp_i*/, 1000)
}
i++;

//再重复几次循环体及后操作
}