前言
在一场面试中,面试官问道:什么是闭包?此时你的脑子里浮现出一场csgo的对局,你正守着B包点,对面五个匪提着冲锋枪直接rush B,你吃满了所有道具和枪线,最后你只能开麦大喊:B包!
好了,现在你已经理解闭包了。
哈哈,开个玩笑,让我来强行解毒一下:此时你就是一个外层函数,而你的队友则是内部函数,现在你已经被乱枪打死了(外层函数已经执行完毕了),你也不知道接下来你的队友会如何发挥(外层函数不知道内部函数有没有执行),但你死了并不能进行任何操作(外层函数执行完要被垃圾回收机制回收),所以你只能给队友留下信息:B包!(所以外层函数给内部函数留下的数据的集合就叫做闭包)。
如果你还是没有理解,别急,这只是前菜,接下来开始正式的讲解:
让我们先看一段代码:
js
var arr = []
for(var i = 0;i<10;i++){
arr[i]=function(){
console.log(i);
}
}
arr.forEach(function(item){
item()
})
你知道这段代码会输出什么结果吗?肯定有人会说输出0到9对吧,那让我们运行一下。
可以看到控制台输出的结果为10个10。现在你心中可能有一万个为什么,不要急,耐心看完这篇文章,你就能知道这其中的奥妙。
首先我们来分析一下这段代码,不就是用for
循环遍历了一个数组arr
,存进去了10个函数嘛,但主要问题不在这,而是再次遍历输出数组时里面所存储的函数执行后会输出个啥,可以看到函数体内容是console.log(i)
,要输出i
,首先就得找到i
,而只有在全局中有定义一个i
,且经过10次for
循环后i现在的值为10,所以就有了上图的结果。
那如果现在我们就要让他输出0到9,该怎么办呢,第一种方法,我们可以把声明变量i
的var
改成let
,再来看看结果
这里涉及到的是块级作用域的知识点,因为let+{}
形成块级作用域, for
循环10次,形成10个块级作用域,函数执行时去当次形成的作用域找i
,于是就输出了0到9。但这不是我们这次主要讲解的知识点,我们要考虑如何用闭包解决这个问题。要理解闭包,我们先来了解一下作用域链。
作用域链
我们再来看一段代码,
js
function bar(){
console.log(a);
}
function foo(){
var a = 100
bar()
}
var a = 100
foo()
因为bar
定义在全局中,所以输出a
时函数会去全局中找a
,结果找到a=100
,即输出100
可以看到运行结果确实是100,那我们来看个更复杂的
js
function bar(){
var myname ='tom'
let test1 = 100
if(1){
let myname='jerry'
console.log(test,myname)
}
}
function foo(){
var myname = 'mike'
let test = 2
{
let test = 3
bar()
}
}
var myname = 'jesper'
let test = 1
foo()
这段代码较为复杂,让我们画图来理解一下
- 首先全局的执行上下文先进入调用栈。
- 按照函数的执行顺序,然后是
foo
的全局上下文进入调用栈。 - 最后是
bar
,执行到bar
函数时,要输出test
和name
。
注意!bar函数寻找变量并不是按普通的从上往下找,而是按照作用域链查找。
- 作用域链,简单来说,作用域链就是一种查找关系,变量环境中有一个内定的outer属性用于指明该函数的外层作用域是谁。
因为bar
函数在全局中创建,所以bar
变量环境中的outer
指向全局,foo
同理。myname
在bar
函数内部找到,值为jerry
但bar
函数内部找不到变量test
,所以按照作用域链去全局中找,得到值为1,所以输出的为1 jerry
垃圾回收机制和闭包
到这里,你应该差不多理解了作用域及作用域链,接下来我们看最后一段代码
js
function foo(){
var name= 'jesper'
function bar(){
console.log(count,age)
}
var count = 1
var age = 18
return bar
}
var age = 20
const baz = foo()
baz()
我们按刚刚的方法画图分析
bar
函数要输出count
和age
,但函数内部并没有变量count
和age
,于是沿着作用域链去foo
函数中找,找到count=1,age=18
。
但结果是不是这样呢,可以说是也不是,如是。因为其实调用栈还有一个规则,即垃圾回收机制,就是当一个函数执行完后,他是应该被销毁的,可以看到上面的foo
函数return
完bar
后,明显执行完毕了,理应被销毁,但如果foo
被销毁了,他内部函数所需变量怎么办呢,因为内部函数离家出走,外部函数并不知道她还有没有执行,既然生下了她,就得对她负责吧,所以外部函数被销毁时,就会在调用栈留下一个小背包,用来存储内部函数可能会用到的变量,用专业点的词说,就叫闭包。
恭喜你现在就已经理解闭包的形成了。
闭包的优缺点
那闭包又有什么作用和缺点呢
作用:
- 实现共有变量:闭包可以创建一个共享的变量空间,让多个函数可以访问同一个变量。这对于实现函数之间共享状态非常有用。
- 做缓存:使用闭包可以实现函数结果的缓存,这样对于相同的输入,可以直接返回之前计算的结果,避免重复计算。
- 封装模块,防止全局变量污染:闭包可以用来创建独立的模块,这些模块不会污染全局命名空间。
缺点:
- 内存泄漏:闭包越来越多,调用栈可用空间越来越小
开头的题目
最后我们回到开头的那道题,是不是用闭包也能轻松解决呢。
js
for(var i = 0;i<10;i++){
function foo(){
var j = i
arr[j]=function(){
console.log(j);
}
}
foo()
}
arr.forEach(function(item){
item()
})
我们只需要在外面用一个函数把储存的函数围起来,这样每次for
循环因为外部函数不知道数组内存储的函数有没有被执行,所以被销毁时都会留下一个闭包,存储当时i的值,这样数组在外部调用存储的函数时,是不是每次都能到形成的闭包中找到对应的值。可以看到最后结果确实为0到9.
结语
总之,我们要知道,闭包的产生就是因为JS的两种机制,即作用域链和垃圾回收机制的冲突,内部函数会沿着作用域链查找变量,而外层函数执行完后因垃圾回收机制需要销毁,所以就有了闭包,即外层函数留下内部函数可能会用到的变量的集合就叫做闭包。