# 闭包
小黄书 P45 定义:一个函数持有对声明它的函数的作用域,使得该作用域能够一直存活,这个引用就叫做闭包
红宝书 P178 定义:闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数
MDN 定义:闭包是指那些能够访问自由变量的函数。(其中自由变量是指函数中使用的,但既不是函数参数 arguments 也不是函数的局部变量的变量,其实就是另一个函数作用域中的变量)
闭包 = 函数 + 函数能够访问的自由变量
ECMAScript中,闭包指的是:
- 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
- 从实践角度:以下函数才算是闭包:
- 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
- 在代码中引用了自由变量
# 闭包产生的原因
作用域链 在 ES5 中有两种作用域 --- 全局作用域和函数作用域,当访问一个变量时,解释器会首先在当前作用域查找标识符,如果没有找到,就去父作用域找,直到找到该变量的标识符或者不再父作用域中,这就是作用域链。每一个子函数都会拷贝上级的作用域,形成一个作用域的链条。
var a = 1;
function f1() {
var a = 2;
function f2() {
console.log(a); // 3
}
}
f1 的作用域指向有全局作用域(window)和它本身,而 f2 的作用域指向全局作用域(window)、f1 和它本身。
作用域是从最底层开始向上找,直到找到全局作用域 window 位置,如果全局作用域还没有就会报错
闭包产生的本质就是:当前环境中存在指向父级作用域的引用 也就是在当前函数的执行上下文中维护了一个作用域链,其中会保存父级的执行上下文中的 AO/VO,即使父级函数执行完毕,执行上下文被销毁之后,但是 JavaScript 依然会让父级执行上下文中的 AO 活在内存中,当前函数依然可以通过它的作用域链找到它,正是因为这样,JavaScript 实现了闭包这个概念
function f1() {
var a = 2;
function f2() {
console.log(a); // 2
}
return f2;
}
var x = f1();
x(); // 2
x 会拿到父级作用域中的变量,输出 2,因为在当前环境中,含有对 f2 的引用,f2 引用了 window、f1 和 f2 的作用域。因此 f2 可以访问到 f1 的作用域的变量
闭包的本质,只需要让父级作用域的引用存在,因此不一定要返回函数才算闭包,也可以:
var f3;
function f1() {
var a = 2;
f3 = function() {
console.log(a);
}
}
f1();
f3(); // 2
让 f1 执行,给 f3 赋值后,现在 f3 拥有了 window、f1 和 f3 本身这几个作用的访问权限,还是自底向上查找,最近是在 f1 中找到了 a,因此输出 2 在这里是外面的变量 f3 存在着父级作用域的引用,因此产生了闭包,形式变了但是本质没变
# 闭包的表现形式
闭包一般变现为 函数中返回一个函数,并被调用
接下来,看这道刷题必刷,面试必考的闭包题:
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
答案是都是 3,让我们分析一下原因:
当执行到 data[0] 函数之前,此时全局上下文的 VO 为:
globalContext = {
VO: {
data: [...],
i: 3
}
}
当执行 data[0] 函数的时候,data[0] 函数的作用域链为:
data[0]Context = {
Scope: [AO, globalContext.VO]
}
data[0]Context 的 AO 并没有 i 值,所以会从 globalContext.VO 中查找,i 为 3,所以打印的结果就是 3。
data[1] 和 data[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]();
当执行到 data[0] 函数之前,此时全局上下文的 VO 为:
globalContext = {
VO: {
data: [...],
i: 3
}
}
跟没改之前一模一样。 当执行 data[0] 函数的时候,data[0] 函数的作用域链发生了改变:
data[0]Context = {
Scope: [AO, 匿名函数Context.AO globalContext.VO]
}
匿名函数执行上下文的 AO 为:
匿名函数Context = {
AO: {
arguments: {
0: 0,
length: 1
},
i: 0
}
}
data[0]Context 的 AO 并没有 i 值,所以会沿着作用域链从匿名函数 Context.AO 中查找,这时候就会找 i 为 0,找到了就不会往 globalContext.VO 中查找了,即使 globalContext.VO 也有 i 的值(值为3),所以打印的结果就是 0。 data[1] 和 data[2] 是一样的道理。