深入学习js系列是自己阶段性成长的见证,希望通过文章的形式更加严谨、客观地梳理js的相关知识,也希望能够帮助更多的前端开发的朋友解决问题,期待我们的共同进步。
如果觉得本系列不错,欢迎点赞、评论、转发,您的支持就是我坚持的最大动力。
开篇
闭包(closure)是 Javascript 语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。
要理解闭包,首先必须理解 Javascript 特殊的变量作用域。
变量的作用域无非就是两种:全局变量和局部变量。
定义
MDN 对于闭包的定义为:
闭包是指那些能够访问自由变量的函数。
那么什么是自由变量呢?
自由变量是指在函数中使用的,但既不是函数的参数也不是函数的局部变量的变量。
由此,我们可以看出来闭包有两部分组成:
闭包 = 函数+函数能够访问的自由变量。
举一个例子:
var a = 1;
function foo() {
console.log(a);
}
foo();
foo 函数可以访问变量 a 但是 a 既不是 foo 函数的局部变量,也不是 foo 函数的参数, 所以 a 就是自由变量。
那么,函数 foo + foo 函数访问的自由变量 a 就已经构成了一个闭包……
还真的是这样!
所以在《JavaScript 权威指南》中就讲到:从技术的角度讲,所有的 JavaScript 函数都是闭包。
上面所说的是理论角度的闭包,其实还有一种实践角度的闭包,让我们看看汤姆大叔翻译的关于闭包的文章中的定义:
ECMAScript 中闭包指的是:
1、从理论角度:所有的函数,因为它们都在创建的时候就将上层上下文的数据保存下来了,哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。 2、从实践角度:以下函数才算是闭包:
- 1、即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)。
- 2、在代码中引用自由变量。
接下来就来聊聊实践上闭包。
分析
让我们来先看一个例子:
var scope = "global scope";
function checkscope() {
var scope = "local scope";
function f() {
return scope;
}
return f;
}
var foo = checkscope();
foo();
首先我们要分析一下这段代码中执行上下文栈和执行上下文的变化。
这里给出简要的执行过程:
1、进入全局代码,创建全局执行上下文,全局上下文压入执行上下文栈。
2、全局执行上下文初始化。
3、执行 checkscope 函数,创建 checkscope函数执行上下文,checkscope 执行上下文被压入执行上下文栈。
4、checkscope 执行上下文初始化,创建变量对象、作用域链、this 等。
5、checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈弹出。
6、执行 f 函数,创建 f 函数的执行上下文,f 执行上下文被压入执行上下文栈
7、f 执行上下文初始化,创建变量对象、作用域链、this 等
8、f 函数执行完毕,f 函数上下文从执行上下文栈中弹出
了解到这个过程,我们应该思考一个问题,那就是:
当 f 函数执行的时候,checkscope 函数上下文已经被销毁了啊(即已经从执行上下文栈中被弹出),怎么还会读取到 checkscope 作用域下的 scope 值呢?
我们了解了具体的执行过程后,我们知道 f 执行上下文维护了一个作用域链:
fContext = {
Scope: [AO, checkscopeContext.AO, globalContext.VO]
};
对的!,就是因为这个作用域链,f 函数依然可以读取到checkscopeContext.AO
的值,说明当 f 函数引用了 checkscopeContext.AO
中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。
所以,让我们再看一遍实践角度上闭包的定义:
1、即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回) 2、在代码中引用了自由变量
面试必刷题
接下来,看这道刷题必刷。面试必考的闭包题目:
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 的值,所以会从 globalConetxt.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:{
dataL[...],
i:3
}
}
跟没改变之前一样,
当执行 data[0]函数的时候,data[0]函数的作用域链发生了改变:
data[0]Context = {
Scope:[AO,匿名函数Context.VO 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] 是一样的道理。
参考:
深入学习JavaScript系列目录
- #1 【深入学习js之——原型和原型链】
- #2 【深入学习js之——词法作用域和动态作用域】
- #3 【深入学习js之——执行山下文栈】
- #4 【深入学习js之——变量对象】
- #5 【深入学习js之——作用域链】
- #6 【深入学习js之——实际开发场景中的this指向】
- #7 【深入学习js之——执行上下文】
- #8 【深入学习js之——闭包】
- #9 【深入学习js之——参数按值传递】
- #10 【深入学习js之——call和apply】
- #11 【深入学习js之——类数组对象与arguments】
- #12 【深入学习js之——创建对象的各种方式以及优缺点】
欢迎添加我的个人微信讨论技术和个体成长。
欢迎关注我的个人微信公众号——指尖的宇宙,更多优质思考干货