深入理解JavaScript系列(7):闭包

介绍

闭包,在外面介绍了前面的之后,就可以了解我们的闭包了。

概念

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域

我们先来简单回顾一下执行上下文的第二个例子吧。

1
2
3
4
5
6
7
8
9
var name = 'hzq';
function checkName() {
var name = 'hzzzzzzzq';
return function fn() {
return name;
};
}

checkName()(); // hzzzzzzzq

我们这上面这段代码来看一下执行上下文栈和执行上下文的情况。

  1. 进入全局代码创建全局上下文,globalContext 入栈
  2. 全局代码执行
  3. 执行 checkName 函数,创建 checkName 函数执行上下文,checkNameContext 入栈
  4. checkName 执行上下文初始化,创建变量对象、作用域链、this 等
  5. checkName 函数执行完毕,出栈
  6. 执行 fn 函数,创建 fn 函数执行上下文,fn 函数上下文入栈
  7. fn 执行上下文初始化,创建变量对象、作用域链、this 等
  8. fn 函数执行完毕,fn 函数上下文弹出
  9. 执行其他全局代码,代码执行完毕,全局上下文弹出

我们看这个过程时,发现 checkName 函数已经不在执行上下文栈中,为什么还可以取到 name 的值呢?

从 《深入理解 JavaScript 系列(4):作用域链》 中,我们了解到,fn 函数,其实维护了一个作用域链,可以看看。

1
2
3
fnContext = {
Scope: [AO, fnContext.AO, globalContext.VO],
};

所有,其实 fn1 取到的就是上层的函数。就是因为作用域链,实现了父级函数即使被销毁,也可以取到值。

我们先举一个简单的闭包使用方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
function fn() {
var count = 1;
return function f() {
return ++count;
};
}

var fn1 = fn();
console.log(fn1()); // 2
console.log(fn1()); // 3
console.log(fn1()); // 4
console.log(fn1()); // 5
console.log(fn1()); // 6

我们的执行过程类似,但是在作用域链中保存着值,可以进行修改,也导致了一个问题,也就是内存删除不干净,内存泄露。

必刷面试题

我们来看一道经典的闭包面试题。

1
2
3
4
5
6
7
8
9
const data = [];
for (var i = 0; i < 5; i++) {
data[i] = function () {
console.log(i);
};
}
data[2](); // 5
data[3](); // 5
data[4](); // 5

这题是我们经常遇到的题目,为什么打印的全是 5 呢?

我们来看一下全局上下文中,执行函数之前的 globalContext,如果对此有问题,可以去看一下前面的几篇文章。《深入理解 JavaScript 系列(2):执行上下文栈》、《深入理解 JavaScript 系列(3):变量对象》、《深入理解 JavaScript 系列(4):作用域链》、《深入理解 JavaScript 系列(5):this》。

我们来看一下上面代码的全局上下文(globalContext) 的内容是什么。

1
2
3
4
5
6
7
globalContext = {
VO: {
data: [...], // item => function
i: 5
},
Scope: [globalContext.VO],
}

这样就一定都打印 5 了吗? 不是的,我们还需要看看 data 中函数的上下文。

1
2
3
4
5
6
7
8
data[0]Context = {
AO: {
arguments: {
length: 0,
},
},
Scope: [AO, globalContext.VO],
}

所以,我们在 data[0] 函数中没有找到结果 i,于是从 global 中寻找,于是找到结果为 5。

1
2
3
4
5
6
7
8
9
10
11
const data = [];
for (var i = 0; i < 5; i++) {
data[i] = (function () {
return function () {
console.log(i);
};
})();
}
data[2](); // 2
data[3](); // 3
data[4](); // 4

这变成了一个立即执行函数。

我们在全局上下文的 VO 是没有变化的。

但是我们来看看 data[0] 函数执行时,其中有一个是匿名函数,我们假设为 NoName 函数,我们来看一下 data[0] 的上下文

1
2
3
4
5
6
7
8
data[0]Context = {
AO: {
arguments: {
length: 0,
},
},
Scope: [AO, NoNameContext.AO, globalContext.VO]
}

在来看看匿名函数的上下文

1
2
3
4
5
6
7
8
9
NoNameContext = {
AO: {
arguments: {
length: 0,
},
i: 0,
},
Scope: [AO, globalContext.VO],
};

所以,这时候执行时,找到了 i = 0,返回结果。

-------------------- 本文结束 感谢阅读 --------------------