深刻学习js系列是本身阶段性成长的见证,但愿经过文章的形式更加严谨、客观地梳理js的相关知识,也但愿可以帮助更多的前端开发的朋友解决问题,期待咱们的共同进步。javascript
若是以为本系列不错,欢迎点赞、评论、转发,您的支持就是我坚持的最大动力。html
闭包(closure)是 Javascript 语言的一个难点,也是它的特点,不少高级应用都要依靠闭包实现。前端
要理解闭包,首先必须理解 Javascript 特殊的变量做用域。java
变量的做用域无非就是两种:全局变量和局部变量。git
MDN 对于闭包的定义为:github
闭包是指那些可以访问自由变量的函数。面试
那么什么是自由变量呢?数组
自由变量是指在函数中使用的,但既不是函数的参数也不是函数的局部变量的变量。微信
由此,咱们能够看出来闭包有两部分组成:闭包
闭包 = 函数+函数可以访问的自由变量。
举一个例子:
var a = 1;
function foo() {
console.log(a);
}
foo();
复制代码
foo 函数能够访问变量 a 可是 a 既不是 foo 函数的局部变量,也不是 foo 函数的参数, 因此 a 就是自由变量。
那么,函数 foo + foo 函数访问的自由变量 a 就已经构成了一个闭包……
还真的是这样!
因此在《JavaScript 权威指南》中就讲到:从技术的角度讲,全部的 JavaScript 函数都是闭包。
上面所说的是理论角度的闭包,其实还有一种实践角度的闭包,让咱们看看汤姆大叔翻译的关于闭包的文章中的定义:
ECMAScript 中闭包指的是:
一、从理论角度:全部的函数,由于它们都在建立的时候就将上层上下文的数据保存下来了,哪怕是简单的全局变量也是如此,由于函数中访问全局变量就至关因而在访问自由变量,这个时候使用最外层的做用域。 二、从实践角度:如下函数才算是闭包:
- 一、即便建立它的上下文已经销毁,它仍然存在(好比,内部函数从父函数中返回)。
- 二、在代码中引用自由变量。
接下来就来聊聊实践上闭包。
让咱们来先看一个例子:
var scope = "global scope";
function checkscope() {
var scope = "local scope";
function f() {
return scope;
}
return f;
}
var foo = checkscope();
foo();
复制代码
首先咱们要分析一下这段代码中执行上下文栈和执行上下文的变化。
这里给出简要的执行过程:
一、进入全局代码,建立全局执行上下文,全局上下文压入执行上下文栈。
二、全局执行上下文初始化。
三、执行 checkscope 函数,建立 checkscope函数执行上下文,checkscope 执行上下文被压入执行上下文栈。
四、checkscope 执行上下文初始化,建立变量对象、做用域链、this 等。
五、checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈弹出。
六、执行 f 函数,建立 f 函数的执行上下文,f 执行上下文被压入执行上下文栈
七、f 执行上下文初始化,建立变量对象、做用域链、this 等
八、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 作到了这一点,从而实现了闭包这个概念。
因此,让咱们再看一遍实践角度上闭包的定义:
一、即便建立它的上下文已经销毁,它仍然存在(好比,内部函数从父函数中返回) 二、在代码中引用了自由变量
接下来,看这道刷题必刷。面试必考的闭包题目:
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] 是同样的道理。
欢迎添加个人我的微信讨论技术和个体成长。