我们来通过图解作用域链,来看看闭包是怎么回事
上代码
1
这个代码在全局作用域中执行。大部分的代码与上篇讲解作用域链的代码一致,(JS-图解作用域链-秒懂)其中的不同点是:
- 内部函数inner引用了外部函数的变量
- outer函数执行完成之后,将内部函数inner的引用return了出去,并且有一个result变量接收了该引用
- 通过result变量,就可以调用inner函数
2
我们来讲讲这个不同点,对函数变量对象的影响。这个影响是形成闭包的关键
3
在JS引擎完成了outer函数的编译后,outer执行上下文的变量对象中,就有了inner函数。inner函数的属性[[Scope]]中指向一个作用域链,作用域链就相当于引用列表。在该引用列表中,第0的位置是outer变量对象的引用,第1的位置是全局变量对象的引用。
我们看到inner函数的[[Scope]]属性,其中有两项,其中一项是Closure--outer函数的变量对象,另外一项是全局变量对象。
4
执行第6行,编译inner函数,生成inner函数的执行上下文。执行inner函数的代码,打印出a变量的值。inner函数执行完毕。inner函数执行上下文被销毁
虽然执行上下文被销毁了,但是堆中的函数体还是健在的。也就是说,在outer的作用域里面,可以再次地调用inner函数
5
执行到第7行,返回inner函数的引用
6
第10行,用result变量接收outer函数返回的引用。outer函数执行上下文销毁。额,等等,销毁了吗,不完全销毁
我们可以看到,当inner执行完毕,它的执行上下文中的变量对象并没有被销毁。为什么?因为还有一个引用指向这个对象。这个引用来自inner函数作用域链。可以理解为有引用指向的对象,不会被GC回收(GC,Grabage Collection)
之所以说是不完全销毁,是因为只有上下文中的变量对象得以保留,但其他的东西都销毁了(this等)
按照上篇的那个例子,在函数outer的执行上下文被销毁了的同时,函数inner也应该被销毁才对。但是这个例子不同,在函数outer执行完成之后,将inner函数的引用返回了给了result。可以理解为:有引用指向inner函数,inner函数就不会被GC回收😁
7
执行到第12行,通过result变量,执行inner函数。编译inner函数,生成inner函数的执行上下文。执行inner函数的代码,打印出a变量的值。inner函数执行完毕。inner函数执行上下文被销毁
8
代码执行到13行。代码执行完毕。
别急,我知道你有些话要说。我在下面的面试题中对闭包做了个总结,快去看看😁
来回答一个关于闭包的经典面试题
闭包是什么时候产生的
如果你仔细看上面的例子,就会发现,在outer
函数对inner函数完成了声明之后,inner函数中就有了[[Scope]]属性,其中第一项我刚才说是outer的变量对象,你仔细看它的命名--Closure闭包。
也就是说,只要内部函数访问了外部变量,这个Closure就在函数声明的时候产生了。这个Closure的存在性和后面的是否返回到外面,以及是否得到执行都没有关系
是不是很惊讶😁
解释:因为inner函数的声明完成后,函数里就有了指向outer变量对象的引用了。一旦有非来自于自身的指针指向自己的变量对象。那么该变量对象的销毁,可能就不会随着所处执行上下文的销毁而销毁了。这不就是闭包的产生么?
不可思议?我们下面来看下面的例子
下面的代码和上面的一样,也有一个inner函数,只是后面并没有执行这个inner函数,也没有将这个inner函数return出去。只是在inner函数声明的下面打印出了inner函数
来看看输出结果
毫不意外,Closure还是产生了😄
回顾下问题:闭包是什么时候产生的?
答:这个Closure就在内部函数声明的时候产生了
你还是不服?好吧,我知道你的不服在哪里。
你大概的意思可能是:只有当内部函数被返回到了外面时,并且外部函数的执行下文都销毁了,但是内部函数还是可以访问到外部函数的变量。这个时候的,装着外部函数变量的变量对象才叫闭包
这个确实有点道理,但你是否知道,无论inner函数是否被return出去,[[Scope]]中的Closure都不会发生变化。也就是说Closure只要存在,自始至终都是不会变的。从代码层面看闭包早就产生了。
《高程》中写道:闭包指的是那些引用了另外一个函数作用域中变量的函数,通常是在嵌套函数中实现的。
好吧,它把闭包说成是函数,这是我没想到的。不过可以确定的一点,闭包的形成与内部函数是否返回到外界无关。
要知道,这个Closure是跟着inner函数体的,只要inner函数体一直存在,Closure就一直存在。这是很可能造成内存泄漏的。
那如何解放这个Closure呢,当没有一个引用指向inner函数的时候,inner函数就会被销毁,Closure就会得到释放啦
所以,《高程》在闭包章节的结尾处又写到:因为背包会保留它们包含函数的作用域,所以比其他函数更占用内存。过度使用背包可能导致内存的过度占用,因此建议仅在十分必要的时候使用。V8等优化的JavaScript引擎会努力回收被闭包困住了内存。不过我们还是建议在使用背包的时候要谨慎。
下篇文章要谈论的内容,就和V8的优化有关
总结
- 在代码层面上看,在内部函数一旦声明,闭包就产生了。闭包的存在与否和内部函数是否返回到外界无关
- 你认为什么是闭包呢?
- V8引擎会对闭包做优化,努力回收被闭包困住的内存
- 下篇文章,就会说V8是如何对闭包做优化的
- 这篇文章中,有哪里我写的让你不明白的,一定要要告诉我,谢谢🙏