0%

十五分钟搞定JavaScript闭包

什么是闭包?

简单通(cuo)俗(wu)来说,闭包就是一个函数,只不过这个函数是在另一个函数内声明的。
这句话是不太严谨,但是此刻我们先这么理解就对了。
举个例子,通常我们声明函数是这样的:

1
2
3
function func1() {
//...
}

如果一个函数是在另一个函数中声明的:

1
2
3
4
5
function func2() {
function func3() {
//...
}
}

那么,这里的func3函数就是传说中的闭包。这个时候,有人就会问,这个闭包func3跟正常声明的func1函数有啥不一样的?
嗯,这个问题问得好。没有什么大区别,除了这一点: ** func3函数可以访问func2函数的作用域 **。
就是这一点点小区别,就给起了个名字?上个世纪六十年代嘛,大家都喜欢搞点噱头。
正是这一个伟大的区别,人们加以利用实现了伟大的跨越。

不过,仔细想想,这也没有什么大不了啊,由于,嵌套的函数可以访问到其外层作用域中声明的变量,func2内部定义的函数func3当然可以访问其父函数func2的变量了。
在这里又要划重点了,** 如果这个func3函数在func2以外也可以调用的话就厉害了 **。
啥?啥啥?啥啥啥?
先不要啥啥啥,我们先实现一下:

1
2
3
4
5
6
7
8
function func2() {
function func3() {
//...
}
// 函数是一等公民(普通对象),当然可以作为返回值了。
return func3;
// 还有别的办法可以把这个函数传递出去,比如在func2之前先声明一个变量,在这里将func3赋给它也是可以的。
}

然后我们再运行一下这个func2()函数,并把结果赋给一个变量func4:

1
const func4 = func2();

啥?啥啥?啥啥啥?func4?!其实也没啥了,这个func4就是func3, 同一个东西的不同名字了。
这样的话,我们就可以在func2以外通过func4来就能调用func3

所以,闭包究竟是啥?闭包是指那些能够可以“记忆”它被创建时候的环境(也就是不管走到哪里,都带着跟着它发家的那些小弟)的函数。

你说的这些我都懂,然而这个东西有什么用?

闭包有什么用?

来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
function func2() {
let a = 1;
function func3() {
let b = 2;
a += b;
console.log( a, b);
}
return func3;
}

const func4 = func2();
func4(); // --> 3, 2
func4(); // --> 5, 2

这里发生了什么事?即使func2函数已经执行完毕了,我们还是能通过func4函数访问并且修改func2函数中定义的变量a。怎么样,很炫吧?

额,并没有看出来这个闭包有什么用?那我们再来一个例子:

1
2
3
4
5
6
7
8
9
10
11
function func2(a) {
function func3(b) {
console.log(a + b);
}
return func3;
}

const func4 = func2(1);
const func5 = func2(2);
func4(100); // --> 101
func5(100); // --> 102

这里又发生了什么事?
我们两次调用func2时传入不同的参数,调用返回的函数也就不同,返回的函数有什么不同呢?他们的访问到的变量a是不同的(对func4,a是1, 对func5,a是2),所以当我执行func4,func5时传入了同样的参数100,返回值却分别是101, 102,原因就在于这两个函数保存着的a是不同的。
明白是明白了,不过这个有什么用?!
… … … … … … …
其实这个闭包很有用,我们以前也偷偷地就用了闭包了,只是当时还没这么叫。
以前用过?对的,如下:

1
2
3
4
5
6
7
function say(message) {
setTimeout(function timeoutHandler() {
console.log(message);
}, 1000);
}

say("我以前用过闭包?嗯,这个就是!");

不要纳尼?,仔细想一想,这里的timeoutHandler函数就是我们神奇的闭包——它是在say函数中定义的,它在say函数执行了一秒以后才执行的,但是依然可以访问到say函数的变量message
通常我们见到的这个函数都是匿名函数,不过这个跟它匿名不匿名没有什么关系。
for循环中也可以使用闭包,比如从1到5,每隔一秒打印一个数字:

1
2
3
4
5
for (let i = 1; i <= 5; i++) {
setTimeout(function timeoutHandler() {
console.log(i);
}, i * 1000);
}

这个是可以顺利实现的,因为ES2015之后,在花括号(块级作用域)中定义一个函数,也可以形成闭包,这就是在文章开头说的不严谨的地方之一,timeoutHandler虽然不是在函数中定义的,但是它在实例化(循环5次就相当于实例化了5个timeoutHandler函数,也就是把它的名字复制给了5个完全不同的变量)时也是形成了闭包。每一个timeoutHandler访问到的i是实例化时的i,因为这个for循环基本上等同于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
let i = 1;
function timeoutHandler() {
console.log(i);
}
setTimeout(timeoutHandler, i * 1000);
}
{
let i = 2;
function timeoutHandler() {
console.log(i);
}
setTimeout(timeoutHandler, i * 1000);
}
{
let i = 3;
function timeoutHandler() {
console.log(i);
}
setTimeout(timeoutHandler, i * 1000);
}
{
let i = 4;
function timeoutHandler() {
console.log(i);
};
setTimeout(timeoutHandler, i * 1000);
}
{
let i = 5;
function timeoutHandler() {
console.log(i);
};
setTimeout(timeoutHandler, i * 1000);
}

这个时候又有同学要问了,那在ES2015之前应该怎么办呢?也不能用let。那就用全世界最xxx的三个字母var呗。

1
2
3
4
5
for (var i = 1; i <= 5; i++) {
setTimeout(function timeoutHandler() {
console.log(i);
}, i * 1000);
}

运行这段代码的结果是啥?结果是每隔一秒输出一个6。等等,5个6?这是咋回事儿呢?因为var它不会限制在一个花括号(块)中,这段代码基本上等同于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{
var i = 1;
function timeoutHandler() {
console.log(i);
}
setTimeout(timeoutHandler, i * 1000);
}
{
var i = 2;
function timeoutHandler() {
console.log(i);
}
setTimeout(timeoutHandler, i * 1000);
}
{
var i = 3;
function timeoutHandler() {
console.log(i);
}
setTimeout(timeoutHandler, i * 1000);
}
{
var i = 4;
function timeoutHandler() {
console.log(i);
};
setTimeout(timeoutHandler, i * 1000);
}
{
var i = 5;
function timeoutHandler() {
console.log(i);
};
setTimeout(timeoutHandler, i * 1000);
i += 1;
}

花括号是没有什么作用的,var和函数都会提升到上面,所以每个函数都是获得的i值都是最终的i,也就是6。

正确的做法是当然是我们的主角闭包:

1
2
3
4
5
6
7
for(var i=1; i<=5; i++) {
(function helper(i) {
setTimeout(function timeoutHandler() {
console.log(i)
}, i * 1000);
})(i);
}

在这里我们声明了函数helper,这个函数一般是匿名函数。声明后立即调用,调用时传入i,封闭在当前的作用域。基本等同于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
{
var i = 1;
function helper(i) {
function timeoutHandler() {
console.log(i)
}
return timeoutHandler;
}
setTimeout(helper(i), i * 1000);
}
{
var i = 2;
function helper(i) {
function timeoutHandler() {
console.log(i)
}
return timeoutHandler;
}
setTimeout(helper(i), i * 1000);
}
{
var i = 3;
function helper(i) {
function timeoutHandler() {
console.log(i)
}
return timeoutHandler;
}
setTimeout(helper(i), i * 1000);
}
{
var i = 4;
function helper(i) {
function timeoutHandler() {
console.log(i)
}
return timeoutHandler;
}
setTimeout(helper(i), i * 1000);
}
{
var i = 5;
function helper(i) {
function timeoutHandler() {
console.log(i)
}
return timeoutHandler;
}
setTimeout(helper(i), i * 1000);
i += 1;
}

这么看来就明白了吧。

所以最后的结论:闭包就是一种特殊的函数,它可以访问到定义它的作用域内的所有变量,即使是在别的地方调用。

参考文章和深入阅读: