从内存管理角度看闭包

113 阅读2分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。 前面有讲到不包含闭包的js函数调用前后内存的变化,具体见下述链接: juejin.cn/post/708756…

这次来看一下包含闭包的函数被调用前后内存的变化,先上一段代码:

var name = "echo";
        function foo() {
            var age = 18;
            return function bar() {
                console.log(age);
            }
        }
        var fn = foo();
        fn();

同样的,在执行全局代码之前,会对其进行解析,这一过程跟上述链接中提到的是一致的。 image.png 然后来看一下代码执行的过程 首先是第一行var name="echo" 这一步会去全局执行上下文中的VO也就是GO中查找name并将其修改为echo

image.png 2-7行为函数定义,并不会被执行

第8行 var fn=foo(); 这一步可以分为两个步骤

  1. 执行foo函数
  2. 将foo函数的返回值赋给fn

先看第一步执行foo函数

在执行foo函数前会创建一个foo函数的执行上下文,并在foo函数中创建foo函数的AO。在内存中的表示如下:

image.png 然后开始执行foo函数,var age=18;

执行这一步回去foo函数的AO中寻找到age并为其赋值为18

image.png 然后将bar函数作为返回值返回。并赋值给了fn。

因此GO中的fn由undefined变为了bar函数的引用,如下:

image.png 这个时候foo函数已经执行完毕是要调用垃圾回收器调用foo函数的执行上下文和堆空间内的foo函数的AO对象销毁掉。但其实最终的内存状态是下面这样的,堆空间的AO并没有被销毁

image.png 这是因为堆空间中的垃圾回收会对栈从根元素进行遍历,若到达不了该对象则将该对象清除。而对于foo函数的AO对象,我们可以来看一下,从栈出发是否可以到达。

首先是全局执行上下文中的VO指向堆中的GO,而GO中的fn执行bar函数对象,bar函数对象的parentScope又指向AO对象,所以是存在一个VO(全局执行上下文)->GO->bar函数对象->AO(foo);所以我们是可以从栈中到达AO中的。所以AO是不会被垃圾回收器清除的。 最后执行fn(),执行之前:

image.png 然后执行内部代码,consoLe.log(age);

会先去fn的VO中查找age,VO指向的是堆中fn的AO,查找不到age,所以会去其父级作用域foo的AO中查找,查找到了age,于是最终会打印18