# 定义
先来看下MDN对闭包的定义:
函数与对其状态即 词法环境(lexical environment) 的引用共同构成闭包(closure)。
换句话说就是
闭包是指那些能够访问自由变量的函数。
那什么是自由变量呢?
自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。
所以,可以看出闭包由两部分组成:
闭包 = 函数 + 函数能够访问的自由变量。
看个例子:
var a = 1;
function foo() {
console.log(a);
}
foo(); // 1
2
3
4
5
6
7
foo 函数可以访问变量 a, 但是 a 既不是函数局部变量也不是函数参数,所以 a 是自由变量。
那么,函数 foo + 函数 foo 访问的自由变量 a 不就构成了一个闭包了吗?
确实是这样!
所以在《JavaScript权威指南》8.6节中就讲到:从技术的角度讲,所有的JavaScript函数都是闭包:它们都是对象,它们都关联到作用域链。
但是这个是从理论技术的角度上来说的闭包,跟实践中闭包还是有所差别,在汤姆大叔 - 深入理解JavaScript系列(16):闭包中给出了闭包的定义:
- 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
- 从实践角度:以下函数才算闭包: i. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回) ii. 在代码中引用了自由变量。
下面重点讨论下实践角度的闭包。
# 实践中的闭包
先看一个《JavaScript权威指南》里面的例子:
var scope = "global scope";
function checkScope() {
var scope = "local scope";
function f() { return scope }
return f;
}
var foo = checkScope();
foo() // local scope
2
3
4
5
6
7
8
我们分析一下这段代码中执行上下文栈和执行上下文的变化:
- 进入全局代码,创建全局上下文,全局执行上下文压入执行上下文栈
ECStack = [
globalContext
]
2
3
- 全局上下文初始化
globalContext = {
VO: [global],
Scope: [globalContext.VO],
this: globalContext.VO
}
2
3
4
5
- 初始化同时,checkScope 函数被创建,保存作用域链到函数内部属性 [[scope]]
checkScope.[[scope]] = [
globalContext.VO
]
2
3
- 执行 checkScope 函数,创建 checkScope 函数执行上下文,checkScope 函数执行上下文被压入执行上下文栈
ECStack = [
checkScopeContext,
globalContext
]
2
3
4
- checkScope 函数初始化上下文:
1). 复制函数 [[scope]] 属性创建作用域链
2). 用 arguments 创建活动对象
3). 初始化活动对象,即加入形参、函数声明、变量声明
4). 将活动对象压入 checkScope 作用域链顶端
同时 f 函数被创建,保存作用域链到 f 函数的内部属性 [[scope]]
checkScopeContext = {
AO: {
arguments: {
length: 0
},
scope: undefined,
f: reference to function f(){}
},
Scope: [AO, globalContext.VO],
this: undefined
}
2
3
4
5
6
7
8
9
10
11
- checkScope 函数执行完毕,checkScope 执行上下文从执行上下文栈弹出
ECStack = [
globalContext
]
2
3
- 执行 f 函数,创建 f 函数执行上下文,f 执行函数压入执行上下文栈
ECStack = [
fContext,
globalContext
]
2
3
4
- f 函数执行上下文初始化,创建变量对象、作用域链,this等
fContext = {
AO: {
arguments: {
length: 0
}
},
Scope: [AO, checkScopeContext.AO, globalContext.VO],
this: undifined
}
2
3
4
5
6
7
8
9
- f 函数执行完毕,f 函数执行上下文栈从上下文栈中弹出
ECStack = {
globalContext
}
2
3
了解完整个过程,需要注意一个问题就是:
到第8步的时候,checkScope 函数上下文已经从执行上下文栈弹出(即销毁了执行上下文),为什么还能读取到 checkScope 作用域下的 scope 值?
那是因为在 JavaScript 中,f 执行上下文维护了一个作用域链:
fContext = {
Scope: [AO, checkScopeContext.AO, globalContext.VO],
}
2
3
正是这个作用域链,f 函数依然可以读取到 checkScopeContext.AO 的值,说明当 f 函数引用了 checkScope.AO 中的值的时候,即使 checkScopeContext 被销毁了,但是 JavaScript 仍然会把 checkScope.AO 存在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。
所以,让我们再看一遍实践角度上闭包的定义:
- 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
- 在代码中引用了自由变量
# 思考题
看一下下面的这道题,会输出什么内容?
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function() {
console.log(i);
}
}
data[0]();
data[1]();
data[2]();
2
3
4
5
6
7
8
9
10
11
结果都是3,我们来分析一下这个结果。
- 当执行到 data0 函数之前,循环已经执行完毕,此时i的值为3,此时全局上下文 VO 为:
globalContext = {
VO: {
data: {...},
i: 3
}
}
2
3
4
5
6
- 当执行 data[0] 函数的时候,data[0]函数的作用域为:
data[0]Context = {
Scope: [AO, globalContext.VO]
}
2
3
data[0]Context 的 AO 没有 i 的值,所以去 Scope 上一层 globalContext.VO 中去查找,i 为 3,所以结果就是 3,同理,data1 和 data2 也是 3。
如果想让函数输出期望的值0、1、2,应该怎么修改?
可以将例子改成用闭包:
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function(i) {
return function() {
console.log(i);
}
})(i)
}
data[0]();
data[1]();
data[2]();
2
3
4
5
6
7
8
9
10
11
12
13
此时,在执行 data[0] 函数的时候,data[0]函数的作用域链已经发生了变化:
data[0]Context = {
Scope: [AO, 匿名函数Context.AO, globalContext.VO]
}
2
3
匿名函数执行上下文AO
匿名函数Context = {
AO: {
arguments: {
0: 0,
length: 1
},
i: 0
}
}
2
3
4
5
6
7
8
9
data[0]Context.AO 并没有i值,就会沿着作用域链从匿名函数Context.AO中查找,这个时候找到为0,终止继续向上查找,结果就为0,data[1]和data[2]同理。
除此之外,还可以借助 ES6 的 let 来实现同样的效果
var data = [];
for (let i = 0; i < 3; i++) {
data[i] = function() {
console.log(i);
}
}
data[0]();
data[1]();
data[2]();
2
3
4
5
6
7
8
9
10
11
这是因为 let 存在暂存性死区,将 for 循环隐式地声明为块作用域,let 不仅将 i 绑定到了 for 循环块中,事实上它将其 重新绑定到循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值。
let 等价于如下代码:
var data = [];
var _loop = function _loop(i) {
data[i] = function () {
console.log(i);
};
};
for (var i = 0; i < 3; i++) {
_loop(i);
}
data[0]();
data[1]();
data[2]();
2
3
4
5
6
7
8
9
10
11
12
13
14
15