根据《高程》中所讲:闭包是指有权访问另一个函数作用域中的变量的函数。
《Javascript 权威指南》中指出,从技术角度讲,所有的 javascript 函数都是闭包。
闭包,之前感觉很神秘,今天我们来揭开它的面纱,看看究竟干了什么!

我们举个例子:

1
2
3
4
5
6
7
8
function scope() {
let a = 1;
return function () {
return a;
};
}
let foo = scope();
foo();

根据前面所写的《执行上下文》中我们可以找到当解析代码时,会执行上下文栈,那我们按照执行上下文来看一下函数内部都做了哪些事情。

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
var scope = [];
scope.push(globalContext = {
this: <Global Object>,
LE: {
ER: {
scope: <func>,
foo: <uninitialized>,
},
outer: null
},
VR: {
ER: {},
outer: null
}
});
scope.push(<scope>, scopefunctionContext = {
this: <Global Object>,
LE: {
ER: {
arguments: {
length: 0
}
},
outer: <globalContext>
},
VR: {
ER: {},
outer: null
}
});
scope.pop();
scope.push(<foo>, foofunctionContext = {
this: <Global Object>,
LE: {
ER: {
arguments: {
length: 0
}
},
outer: <scopefunctionContext>
},
VR: {
ER: {},
outer: null
}
});
scope.pop();

其实会很好奇,scope 函数执行完后明明已经在执行栈中移除了,为什么 foo 函数依旧能访问到其内部的变量。是因为 foo 函数的对外部引用是 scope 的词法环境,这个环境还没有消失。正因为 JS 有这个特点,所以才会生成闭包这个概念。

下面举几个常用的面试题,来自《高程》:

1
2
3
4
5
6
7
8
9
10
11
12
function createFunctions() {
var result = new Array();
for (var i = 0; i < 10; i++) {
result[i] = function () {
console.log(i);
};
}
return result;
}
createFunctions().forEach((item) => {
item();
});

运行上述之后,发现最后输出 10 个 10,为什么不是 0-9 呢?我们来分析一下,已 result[0]为例,在 result[0]运行之前,全局上下文是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var scope = [];
scope.push(globalContext = {
this: <Global Object>,
LE: {
ER: {
createFunctions: <func>,
},
outer: null
},
VR: {
ER: {
result: [....],
i: 10
},
outer: null
}
})

当 result[0]运行时,它的函数上下文发生变化:

1
2
3
4
5
6
7
8
9
10
11
functionContext = {
this: <Global Object>,
LE: {
ER: {},
outer: <createFunctionsContext>
},
VR: {
ER: {},
outer: <createFunctionsContext>
}
}

由于 result[0]中没有定义 i,所以就会向外部的词法环境中查找,最后找到 i,输出 10。

如果想输出预期结果 0-9,高程中也给出了解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createFunctions() {
var result = new Array();
for (var i = 0; i < 10; i++) {
result[i] = (function (num) {
return function () {
console.log(num);
};
})(i);
}
return result;
}
createFunctions().forEach((item) => {
item();
});

当再次执行 result[0]时,这个匿名函数的上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
functionContext = {
this: <Global Object>,
LE: {
ER: {
匿名function: <func>
},
outer: <createFunctionsContext>
},
VR: {
ER: {
arguments: {
0: 0,
length: 1
},
num: 0
},
outer: <createFunctionsContext>
}
}

当匿名函数执行时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
匿名functionContext = {
this: <Global Object>,
LE: {
ER: {},
outer: <functionContext>
},
VR: {
ER: {
arguments: {
length: 0
},
num: 0
},
outer: <functionContext>
}
}

同样其内部没有 num 变量,那它就会去外部的此法环境中查找,找到了 functionContext,functionContext 内部的 num 为 0,则输出 0。这样就完美解决了这个问题,也体现了闭包的作用,但现在有 let 了这种就用的很少了。闭包虽然能解决一些问题,但是尽量还是要少用闭包,因为其外部的词法环境已经销毁了,但其内部还在引用,这样的话闭包过多会造成内存泄漏,最简单直接的办法就是,执行完后设置为 null,这样就销毁了。