什么是闭包?
在MDN中对闭包的定义为:
闭包是那些能够访问自由变量的函数
那什么是自由变量呢?
自由变量是指在函数中使用到了的,但是既不是函数的参数也不是函数的局部变量的变量
所以闭包=函数+函数访问了自由变量
let a = 1;
function foo() {
console.log(a);
}
foo();foo函数可以访问变量a,但是a既不是foo这个函数里面的局部声明的变量,也不是foo函数的入参变量,所以那么a就可以称为自由变量。
所以在《JavaScript权威指南》中就讲到:从技术的⻆度讲,所有的JavaScript函数都是闭包。
但是,这是理论上的闭包,其实还有⼀个实践⻆度上的闭包。
ECMAScript中,闭包指的是:
1. 从理论⻆度:所有的函数。因为它们都在创建的时候就将上层上下⽂的数据保存起来了。哪怕是简
单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问⾃由变量,这个时候使⽤最外
层的作⽤域;
2. 从实践⻆度:以下函数才算是闭包:
1. 即使创建它的上下⽂已经销毁,它仍然存在(⽐如,内部函数从⽗函数中返回);
2. 在代码中引⽤了⾃由变量;
接下来就来讲讲实践上的闭包
let 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函数的上下文已经被销毁了,那为什么f中还可以获取到checkscope作用域下scope这个变量呢?
原因:可以看第7步,在创建f的执行上下文的时候,创建了变量对象,作用域链,this,重点就在这个作用域链里面,里面存储了当前作用域下的变量对象VO(执行的时候VO变成了AO,VO和AO本质一样,可以理解为不同状态),和上层直到顶层作用域下的所有VO。这里f的作用域链
fContextChain = {
Scope: [AO, checkscopeContext.AO, globalContext.VO],
}因为这个作用域链,f函数依然可以读取到CheckscopeContext.AO,说明当f函数引用CheckscopeContext.AO中的值的时候,即使CheckscopeContext这个上下文已经执行完毕,被销毁了,但是js依然会让CheckscopeContext.AO存活在内存中,这也是为什么闭包使用不当,会造成内存泄漏的问题。f函数通过作用域链找到它,正是因为JS实现了这一点,从而实现了闭包的概念。
所以,让我们再看⼀遍实践⻆度上闭包的定义:
1. 即使创建它的上下⽂已经销毁,它仍然存在(⽐如,内部函数从⽗函数中返回);
2. 在代码中引⽤了⾃由变量;
训练题
var data = [];
for (var i = 0; i < 3; i++){
data[i] = function (){
console.log(i);
};
}
data[0]();
data[1]();
data[2]();根据作用域链来做下这道题,在执行data[0]()的时候,全局上下文的VO为:
globalContext={
VO:{
data:[],
i:3
}
}当执行到data[0]()的时候返回的这个函数的作用域链
data[0]Context={
Scope:[AO,globalContext.VO]
}所以在执行console.log(i)的时候,在Scope中自身的AO(VO)里面找不到i这个变量,就随着这个作用域链往上面找,然后再globalContext.VO找到了i这个名称的变量,所以就打印globaContext.VO里面的i,即3
现在我们改下代码,利用闭包
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]();现在我们再来分析下:
globalContext={
VO:{
data:[],
i:3
}
}这个全局的执行上下文的VO没有变化
data[0]Context={
Scope:[AO,匿名函数Context.AO,globalContext.VO]
}现在再来看data[0]的作用域链,发现里面多了个匿名函数的AO
匿名函数Context={
VO:{
i:0//传进来的形参为多少,这个i变量就被赋值为多少
}
}然后data[0]通过作用域链查找到匿名函数的AO时候,在里面找到了i这个名称的变量,就不继续找了,这个就是闭包其它的一种作用,在日常开发中会用到。
如果上面的那种IIFE的调用方法没有看懂,那我换一种通俗的

这个是我在控制台打印的,只是比IIFE多用了()调用了一次,第一次打印的undefined也是正确的,因为我没有在调用返回函数的时候传值,再下面就进行了传值,和IIFE相同的输出。