JS-图解闭包-秒懂

222 阅读6分钟

我们来通过图解作用域链,来看看闭包是怎么回事

上代码

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的优化有关

总结

  1. 在代码层面上看,在内部函数一旦声明,闭包就产生了。闭包的存在与否和内部函数是否返回到外界无关
  2. 你认为什么是闭包呢?
  3. V8引擎会对闭包做优化,努力回收被闭包困住的内存
  4. 下篇文章,就会说V8是如何对闭包做优化的
  5. 这篇文章中,有哪里我写的让你不明白的,一定要要告诉我,谢谢🙏