如果你是一名前端,你一定会接触到闭包,自己学习过程中或者几乎每次面试过程中,都会涉及到这个概念,但是你真的掌握了吗?今天我们就来一起谈谈闭包。
什么是闭包?
先来一点专业的解释,闭包是 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
特性,相信看完这篇文章,已经明白闭包的含义以及实际表现,希望大家有所收获。
最后我抛出一个延伸的问题:闭包有哪些实际应用呢?哈哈