深入闭包

定义

《JS忍者秘籍》中对闭包的定义为:

闭包实际上是一个作用域,在创建时允许其自身的变量和函数访问其自身之外的变量时的作用域。

所以,闭包是由两部分组成的:

闭包 = 函数 + 函数能够访问的自有变量

看个例子:

1
2
3
4
5
6
7
var outerValue = 1; // 全局作用域声明一个变量

function outerFunction() { // 在全局作用域中声明一个函数
console.log(outerValue); // 1
}

outerFunction(); // 执行该函数

该函数是能够访问到自身之外的变量outerValue的,这个时候我们其实已经创建了一个闭包!只不过,这只是理论上的闭包,还有一个实践上的闭包。

  1. 从理论上:所有的函数,因为它们在创建的时候就将上层上下文的数据保存起来了,哪怕是简单的全局变量也是如此,因为函数中访问自由变量就相当于是在访问自由变量,这个时候使用最外层的作用域。

  2. 从实际上,以下函数才算是闭包:

    i. 即使创建它的上下文已经销毁,他仍然存在(比如,内部函数从父函数中返回)

    ii. 在代码中引用了自有变量

必刷题

1
2
3
4
5
6
7
8
9
10
11
var divs = document.getElementsByTagName("div");

for(var i = 0; i < divs.length; i++) {
div[i].onclick = function() {
alert(i);
};
}

data[0](); // 3
data[1](); // 3
data[2](); // 3

这么理解吧~

在激活data[0]之前,全局上下文的VO为:

1
2
3
4
5
6
globalContext = {
VO: {
data: [···],
i: 3
}
}

当激活data[0]函数的时候,data[0]函数的作用域链(scopeChain)为:

1
2
3
data[0]()Context = {
Scope: [AO, globalContext.VO]
}

而data[0]Context的AO中并没有i值,所以会从globalContext.VO中查找,此时i为3,所以打印结果为3,data[1]和data[2]也是一样。

改成闭包~

1
2
3
4
5
6
7
8
9
10
11
12
13
var divs = document.getElementsByTagName("div");

for(var i = 0; i < divs.length; i++) {
div[i].onclick = (function() {
return function(j) {
alert(j);
};
})(i);
}

data[0](); // 0
data[1](); // 1
data[2](); // 2

在激活data[0]之前,全局上下文的VO为:

1
2
3
4
5
6
globalContext = {
VO: {
data: [···],
i: 3
}
}

当激活data[0]函数的时候,data[0]函数的作用域链(scopeChain)为:

1
2
3
4
5
6
7
8
9
10
11
12
13
data[0]()Context = {
Scope: [AO, 匿名函数Context.VO, globalContext.VO]
}

匿名函数Context = {
AO: {
arguments: {
0: 0,
lenth: 1
},
i: 0
}
}

data[0]Context的AO并没有i值,所以会沿着作用域链从匿名函数Context.VO中去找,而匿名函数中如果有就直接用,没有就沿着链继续找globalContext.VO,但是这个例子在匿名函数Context.VO中找到了,所以即使globalContext.VO也有i值(为3),也不会利用,所以data[0]函数的打印结果就是0。

data[1]和data[2]一样。

!!!划重点

  1. 循环不会创建一个执行上下文,所以不会有VO/AO
  2. var声明的循环变量不会将变量与循环块绑定,可以使用let关键字将for循环的块隐式地声明为块作用域(原理:循环变量在循环过程中不止被声明一次,每次迭代都会声明。之后的每次迭代都会使用上一次循环迭代结束时的值来初始化这个变量)

总之!一句话~闭包,其实是跟词法作用域紧密相关的一个概念,通过维护作用域链中涉及的变量的存活,从而导致能在执行上下文栈销毁后访问到变量的值,这也就是常说的内存常驻问题。后续再讲啦~