闭包

1 什么是闭包

关于什么是闭包有下面两种定义方式:

  1. 有能力引用外部变量的函数叫做闭包,因为 js 的函数都具有此能力,故 js 的所有函数都可称作做闭包
  2. 引用了外部变量的函数叫做闭包

个人更倾向于第二种定义方式,有下面两个原因:

  1. 第二种定义会使得理解 js 中的闭包相关问题时更顺畅
  2. 只有当 js 函数引用了外部变量时,其作用域链 [[scope]] 才会包含一个叫做 Closure 的对象(Closure 对象中存储了被引用的外部变量,稍后会有图示)。

在下面示例中,B1 就是一个闭包(因为 B1 引用了外部函数 A 的变量),可以看到 B1 的作用域链包含了一个 Closure 成员。

1
2
3
4
5
6
7
8
9
10
function A(){
var aVar = 'aVar';

return function B1(){
console.log(aVar);
}
}

var B1Alias = A();
console.dir(B1Alias);
  • 当 B1 没有被外部引用时,函数 A 执行完后,A 的局部变量和 B1 会被释放。

  • 当 B1 被外部变量(B1Alias)引用时,即使函数 A 已经执行完成, B1Alias 依旧会将 A 的变量(aVar)保留下来。而函数 A 执行完,其内部的变量却没有被释放,这就是闭包被关注的根本原因

2 闭包的用途

2.1 封装私有变量、方法

js 中可以使用函数来封装模块,其实就是利用了闭包的特性。下面代码就使用函数封装了内部的私有变量 privateCounter 和私有方法 changeBy。外部只能通过 makeCounter 返回的公共方法操作私有变量。

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
var makeCounter = function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
};

var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */

3 闭包中常见的问题

在使用闭包时,很容易出现一些意料之外的执行结果,并且影响性能,容易造成内存泄露,所以应该尽量少使用闭包。

3.1 在循环中创建闭包

在循环中创建闭包时,如果将循环代码直接用于闭包,则会使所有闭包指向同一个变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function constFuns() {
var funs = [];

for (var i = 0; i < 10; i++) {
funs[i] = function() {
return i;//访问外部函数的变量,使其成为一个闭包
};
}
return funs;
}
var funs = constFuns();
console.dir(funs[5]()); //期望输出5 结果输出10
console.dir(funs);
/*
上面代码中,因为 funs 数组中的所有闭包函数在同一个外部函数中定义,所以这些闭包的 Closure 属性共享相同的变量 i,而这个变量 i 是对外部函数变量 i 的引用,而不是复制,当外部函数执行完成时,变量 i 的值变成了 10,导致所有闭包内的变量 i 都变成了 10。
*/

解决上面问题的方法:

1、使用 es6 的 let 语法,从而防止产生闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//使用let填坑
function constFuns() {
var funs = [];

for (let i = 0; i < 10; i++) {
//使用了外部函数的变量时,才会使嵌套函数成为闭包。
//let 不会进行变量提升,也就是说 i 不是外部函数 constFuns 的变量,故循环里面的匿名函数并不是闭包
funs[i] = function() {
return i;
};
}
return funs;
}
var funs = constFuns();
console.dir(funs[5]());
console.dir(funs);

可以看到,匿名函数的作用域链中,并没有包含 Closure 成员,而是包含了并不共享的 Block 成员。从而避免了 Closure 共享所带来的问题。

2、使用更多的闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function constFuns() {
var funs = [];
function inner(i) {
return function() {
return i;
};
}

for (var i = 0; i < 10; i++) {
funs[i] = inner(i);
}

return funs;
}

var funs = constFuns();
console.log(funs[5]());
console.dir(funs);

3.2 影响性能

闭包会对降低程序的处理速度、增加对内存的使用。

3.3 内存泄露

代码中有循环引用时,容易操作内存泄露。

4 注意

4.1 嵌套函数没有使用父函数的变量时,不会产生闭包属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function A(aArg){
var aVar = 'aVar';

//下面两个嵌套函数都没有使用父函数的变量
function B1(b1Arg){
var b1Var = 'b1Var';
}

function B2(b2Arg){
var b2Var = 'b2Var';
}

console.dir(B1);
console.dir(B2);
}

A();

输出:

4.2 嵌套函数在引用父函数的变量后,会产生闭包属性,并且同一外部函数内的嵌套函数共享同一个作用域链(即都会产生闭包属性)。

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
function A(aArg){
var aVar = 'aVar';

/*嵌套函数 B2 引用父函数的变量
嵌套函数 B1 没有引用父函数的变量
更深一级的嵌套函数 C1 没有引用任何父函数的变量

结果:B2 对父函数变量的引用,使其产生了 Closure 属性,并且使平级的 B1 、内部的 C1 都产生了 Closure 属性

*/
function B1(b1Arg){
var b1Var = 'b1Var';

function C1(c1Arg){
var c1Var = 'c1Var';
}

console.dir(C1);
}

function B2(b2Arg){
var b2Var = 'b2Var';
console.log(aVar);
}

console.dir(B1);
console.dir(B2);
B1();
}

A();

chrome 输出: