如果你是一名前端,你一定会接触到闭包,自己学习过程中或者几乎每次面试过程中,都会涉及到这个概念,但是你真的掌握了吗?今天我们就来一起谈谈闭包。
什么是闭包?
先来一点专业的解释,闭包是 JavaScript 中的一项功能,其中内部函数可以访问外部(封闭)函数的变量。内部函数可以访问外部函数的变量这个现象就被称为闭包。
闭包有三大特点:
- 它可以访问自己的作用域
- 它可以访问外部函数的变量
- 它可以访问全局变量
对于外行来说,压根不能理解这样的现象,因为这看起来太玄学了。
那到底什么是真正的闭包?
让我们先来看看这个简单的闭包代码例子:
js
function outsideFn() {
const b = 10;
function insideFn() {
const a = 20;
console.log(a+b);
}
return insideFn;
}
const x = outsideFn();
我们有两个函数
outsideFn函数是外部函数,它有变量b,并且在函数内部声明了内部函数insideFn,并返回了insideFn函数insideFn则是内部函数,它有变量a,在函数内部,它会去访问变量b,最终打印出a+b的结果
从直观的角度来看,变量 b 的范围仅限于 outsideFn 函数,变量 a 的范围仅限于 insideFn 函数。
好,接下来,我们开始执行 outsideFn 函数,因为该函数有返回值,所以我们用一个变量 x 来存储返回结果。
js
const x = outsideFn();
让我们逐步看看调用 outsideFn 函数时会发生什么:
- 初始化变量
b,并且将其值赋值为10,其可访问范围仅局限于outsideFn函数, - 初始化
insideFn函数,这个是一个函数声明,所以不会直接执行该函数, - 最后,返回了
insideFn,它会去寻找名为insideFn的变量,结果发现是个函数,所以就直接返回了整个函数体,
这里要说明一点,return 语句不会直接执行内部函数,仅当后跟 () 时才执行函数
return语句返回的内容存储在变量x中。
按照上面的逻辑,x 的内容应该如下:
js
function insideFn() {
const a = 20;
console.log(a+b);
}
outsideFn函数执行完毕,其中的所有变量都将不复存在。
请注意,最后一步对于理解很重要。在 Javascript 中,一旦函数完成执行,在函数作用域内定义的任何变量都不再存在。也就是说,函数内部定义的变量的生命周期就是函数执行的生命周期。所以按道理来说,在 console.log(a+b) 中,变量 b 仅在 outsideFn 函数执行期间存在。一旦 outsideFn 函数执行完毕,变量 b 就不再存在,这很符合常识,对吧。
接下来,我们梅开二度,我们再次执行一次 outsideFn 函数,但是这次我们把结果保存在一个新的变量 y上面:
js
const y = outsideFn();
执行完成之后,我们得到了 x 和 y 两个函数,因为他们都是执行outsideFn 函数得到的结果。
这里先甩出一个问题,x 和 y 是同一个函数吗?可以先思考一下,然后带着问题继续往下看。
现在,我们来执行一下 x 函数:
js
x();
x 函数本质上就是 insideFn 函数,所以我们执行 x 函数其实就是执行 insideFn 函数。接下来我们一步一步分析一下insideFn 函数的执行过程:
- 初始化 变量
a,并将其值设置为 20,其可访问范围仅局限于insideFn函数, JavaScript现在尝试执行a + b。这就是事情变得有趣的地方。JavaScript知道a存在,因为刚刚创建了它。但是,变量b不再存在。由于b是外部函数的一部分,因此b仅在outsideFn函数执行时存在。由于outsideFn函数早在我们调用x函数之前就执行完成了,因此outsideFn函数范围内的任何变量都不再存在。结果呢,我们会发现,成功的打印了a+b的值:

那么,JavaScript 是如何找到变量 b 的呢?
这,就是闭包
insideFn 函数可以访问 outsideFn 函数里面的变量,其实这里面是作用域链在起作用,简单来说,就是 insideFn 函数的作用域链里面,保存了insideFn 函数的作用域链:

在执行 a + b 时,先在 insideFn 函数的作用域里面寻找,结果没找到,接着就开始在 insideFn 函数的作用域链上面找,接着找到了outsideFn 函数的作用域,最终找到了 变量 b,最终成功执行 a + b。
我们可以通过在 Google Chrome 上面打开命令行执行一下 insideFn 函数:

通过上面图片可以看到,insideFn的 [[Scopes]]属性,存在一个 Closure (outsideFn)属性,而在该属性里面,即可找到 { b: 10 }。而这,就是闭包。
现在让我们重新来看看闭包的特点:
- 它可以访问自己的作用域
- 它可以访问外部函数的变量
- 它可以访问全局变量
更进一步
你应该注意到,前文提到了执行了两次 outsideFn 函数,分别得到了 变量 x 和变量 y,那么 x 和 y 是同一个函数吗?
js
console.log(x===y);

不知道你回答正确了没有?事实上,x 和 y 不是同一个函数,虽然他们是同一个函数体,但是 javascript 为他们分别创建了两个不同的内存区域。接下来,我们再来看看这部分代码:
js
function outsideFn() {
var b = 10;
function insideFn() {
var a = 20;
console.log("a= " + a + " b= " + b);
a++;
b++;
}
return insideFn;
}
const x = outsideFn();
const y = outsideFn();
x();
x();
x();
y();
我们在 insideFn 函数里面打印了一下 变量 a 和 变量 b 的值,接着分别执行 a++ 和 b++。
注意,我们将
a和b都改成了var变量
我们分别调用三次 x 函数和一次 y 函数。好,现在开始无奖竞猜,最终的输出结果是什么?

结果是:
js
a= 20 b= 10
a= 20 b= 11
a= 20 b= 12
a= 20 b= 10
答对了吗?让我们一步步检查这段代码,看看到底发生了什么。
第1次调用 outsideFn 函数:
- 初始化变量
b,并将其设置为 10 (我们把这个 b暂时叫做b1) - 返回
insideFn函数,并将其保留在 变量x里面,这个时候x会保存insideFn函数的作用域。 outsideFn函数执行完毕,其所有变量都不再存在。
第2次执行 outsideFn 函数:
- 初始化变量
b,并将其设置为 10 (我们把这个b暂时叫做b2) - 返回
insideFn函数,并将其保留在 变量y里面,这个时候y会保存insideFn函数的作用域。 outsideFn函数执行完毕,其所有变量都不再存在。
接下来开始执行:
js
x();
x();
x();
y();
第1次执行 x 函数:
- 初始化变量
a,并将其设置为 20 - 打印 变量
a和b的值:
js
a= 20 b= 10
- 变量
a和b增加1 x函数执行完毕,其所有变量都不再存在。
x函数执行完毕,其所有变量都不再存在。那么变量a就不存在了,但是变量b却仍然存在,因为x函数 还保留着outsideFn函数的作用域链。
第2次执行 x 函数:
- 初始化变量
a,并将其设置为 20 - 打印 变量
a和b的值:
js
a= 20 b= 11
这里变量
b就是取自作用域链,而我们第1次执行的时候,将变量b加1了,所以这里打印b=11。
- 变量
a和b增加1 x函数执行完毕,其所有变量都不再存在。
第3次执行 x 函数:
- 初始化变量
a,并将其设置为 20 - 打印 变量
a和b的值:
js
a= 20 b= 12
这里变量
b就是取自作用域链,而我们第1次执行的时候,将变量b加1了,所以这里打印b=12。
- 变量
a和b增加1 x函数执行完毕,其所有变量都不再存在。
这就是闭包的作用和实际表现,你现在理解了吗?
接下来我们再来看看 y 函数的执行:
- 初始化变量
a,并将其设置为 20 - 打印 变量
a和b的值:
js
a= 20 b= 10
- 变量
a和b增加1 y函数执行完毕,其所有变量都不再存在。
x函数执行完毕,其所有变量都不再存在。那么变量a就不存在了,但是变量b却仍然存在,因为x函数 还保留着outsideFn函数的作用域链。
这个时候 变量 b 变成10了,也就是说,b1 和 b2 不是同一个变量,这也印证了,x 和 y 不是同一个函数。
最后
闭包真的是非常有趣的 javascript 特性,相信看完这篇文章,已经明白闭包的含义以及实际表现,希望大家有所收获。
最后我抛出一个延伸的问题:闭包有哪些实际应用呢?哈哈