聊聊闭包-你不知道的那些事

952 阅读6分钟

引言

JavaScript的闭包是个很奇特的存在,这在我们平常学的其它语言中是不曾见过的;当然,也不用太神话它,觉得这是个什么牛逼的存在;不管遇到什么,只要在心中默念“这不过如此”,接下来就好学了。闭包本身比较简单,本文重在聊一聊为什么闭包得以存在,以及闭包的背后那些你可能会忽略的一些细节。

首先我们先宏观的认识一下闭包:

闭包即引用了作用域外的函数,准确来说应该是引用外部变量的函数+被引用的外部变量组成的整体;特性是即使外部函数被销毁,被引用的外部变量仍然存在。为什么会存在,不随着外部函数一起销毁呢?That's a question。所以研究闭包,必然从这里开始:垃圾回收

垃圾回收

为了不让问题变得太复杂,这里只简单讲讲回收算法;大家可能听过标记清除算法(没有听过的可以看看红宝书,不看也行,我稍微讲讲)。标记清除算法大致就是先给每个变量做标记,然后从全局作用域开始(这也是区别于引用计数,不会产生循环计数的关键所在)往下进行遍历,类似dfs;如果能够遍历到某个变量,说明它可被访问,取消其标记;之后仍然被标记的变量则被回收。(之后垃圾回收程序还会整理内存碎片,而且v8采用的垃圾回收算法是分代垃圾回收算法,将内存分为新生区和老生区,新生区有分为from和to...;当然这些对本问题不重要,感兴趣的可以看看三元的博客)。反正简单来说就是,因为可以被遍历到,所以没被清除。为什么可以遍历到呢?

可能有人开始瞎白话了,JavaScript采用的是静态作用域,静态作用域在函数定义时就已经确定其作用域了blabla。讲了和没讲一样。

让我们来看看红宝书是怎么讲的:闭包和作用域链密切相关,假设内部函数b定义在外部函数a的内部,它是一个内部函数,内部函数会将包含函数的活动对象添加到自己的作用域链中,这个过程大致是这样的:

  • 外部函数执行上下文入栈,创建变量对象
  • 进入代码执行阶段,激活变量对象
  • 内部函数此时会将全局变量对象和外部活动对象预添加到[[scope]]
  • 等到内部函数被调用时,将[[scope]]中的变量对象取出加入自己的作用域链,再将自己的活动对象推入作用域链前端,形成最终的作用域链 而b实际上是通过作用域链访问到的value;原因就在于在上面第三步时,内部函数预添加了外部作用域的变量对象。

关键来了:

内部函数被外部函数返回,假设被全局作用域某个变量接收,根据标记清除算法,从全局作用域开始dfs,一直可以层层追溯到外部函数的变量对象,所以不会被清除。这里不一定非得被全局变量接收,只要从全局作用域开始,能被访问到就行,比如被加入到了宏任务队列等待执行。

细心的同学注意到了上面提到的几个词:变量对象、活动对象、执行上下文、作用域链、内部函数,下面逐一解释。

  1. 执行上下文(Execution context)

    • 执行上下文有两种,全局执行上下文(window对象)和函数执行上下文;每个上下文都有一个关联的变量对象(全局执行上下文的变量对象也是window对象),变量对象中存储着该执行上下文中定义的的所有变量和函数。

    • 执行上下文的生命周期分为三个阶段:

      1. 创建阶段(创建变量对象,确定this指向以及其他需要的状态)
      2. 代码执行阶段(此时变量对象被激活为活动对象,活动对象中的属性此时能被访问)
      3. 销毁阶段(回收内存空间)
    • 变量对象的创建过程也分为几个阶段:

      1. 创建arguments对象
      2. 为函数形参创建属性
      3. 搜索该执行上下文中的所有函数声明并创建属性
      4. 搜索该执行上下文中所有的变量声明并创建属性
      //举个例子
      function foo(a) {
        var b = 2;
        function c() {}
        var d = function() {};
      }
      foo(1);
      //将调用过程拆解:
      //执行上下文入栈,进入创建阶段,创建变量对象如下:
      VO = {
          arguments: {
              0: 1,
              length: 1
          },
          a: 1,
          b: undefined,
          c: reference to function c(){},
          d: undefined
      }
      //进入代码执行阶段,成为活动对象,如下:
      AO = {
        arguments: {
              0: 1,
              length: 1
          },
          a: 1,
          b: 2,
          c: reference to function c(){},
          d: reference to function (){}
      }
      
    • 变量对象和活动对象,它们本质是同一个东西,只是处于执行上下文不同的生命周期。变量对象是引擎意义上的,在JavaScript环境中是无法访问的,当执行上下文进入代码执行阶段,它所关联的变量对象会被激活,此时就成了活动对象

  2. 作用域链

    • JavaScript采用词法作用域,也就是静态作用域

    • 作用域就是可访问变量、函数、对象的集合,javascript能定义全局作用域和局部作用域

    • 作用域链,顾名思义,作用域的链表,准确来说是一个包含指针的列表,每个指针指向一个变量对象。我们都引用过全局变量,其实这便是访问了作用域链的结果,当引用某个变量时,首先会查找当前活动对象,逐层向外直至全局变量对象,这便是作用域链的作用。你可能会问作用域和活动对象是什么关系?我只能告诉你,JavaScript作用域是一个静态的概念,在函数定义时就已经确定了,它规定了如何查找变量。活动对象则是一个实体,是在函数调用时动态创建,会被加入到内部函数的作用域链当中,只要了解这一点即可,至于它们之间到底什么关系我形容不出来,意会,其实也没必要纠结。

    • 可以看看这个例子:了解什么是静态作用域

      var value = 1;
      
      function foo() {
          console.log(value);
      }
      
      function bar() {
          var value = 2;
          foo();
      }
      
      bar();
      

      结果为1,你做对了吗?

      原因:foo并不是定义在bar中的,如果你搞混了,说明你对闭包还是有误解,再回头看看。

  3. 内部函数

    见名知意,嵌套在另外一个函数中的函数

参考文献

红宝书

www.jianshu.com/p/330b1505e…