作用域链与闭包

  读了这篇博文地址后,对作用域和闭包终于有一了些了解。之前看各种文章,让我以为闭包是因为内部引用变量,导致变量无法在外部访问,而通过内部函数可以被外部访问导致被引用的变量可以间接访问。现在看来,这只能说是闭包的一种外在表现,跟闭包本身没有任何关系的(在You don't know JS中理解与本文不一致,书中认为闭包实际上是作用域,由于内部函数引用导致外部函数在栈中移出后其作用域仍可被访问,这个作用域被称之为闭包。这样理解的话本文查看的应该是chrome的一种实现,即不管有没有闭包,都会生成一个叫closure的变量,这个变量就是作用域,只是chrome进行了优化,便于垃圾回收,移除了部分非使用的变量。)。

  下面总结一下作用域链和闭包。

1. 执行至闭包之前的整个过程

  更为详细的可参见那篇博文,这里只是简单总结下。在浏览器执行解析代码时,分为编译和代码执行两个部分。编译阶段负责对语法解析,作用域链分析,语法优化。代码执行阶段中,先为函数创建上下文,然后执行代码。创建函数上下文时,要进行:创建变量对象、创建作用域链、确定this的指向。

  变量对象(VO)创建过程:建立arguments对象,并将数据挂载到到arguments上 -> 针对函数内部声明的函数、变量,先进行挂载声明的函数,然后挂载声明的变量(如果名称冲突则不覆盖) 

       变量对象在函数执行之前是不可访问的,执行代码时,变量对象转变为活动对象,所有变量获取均从活动对象中取得,如果没有,则会抛异常。

2. 作用域链创建与闭包

       作用域链的生成规则是在编译阶段就确定了的。当函数创建上下文时,除创建变量对象外,还会产生一个闭包(closure)。这个闭包会在子函数中被加入到作用域链中。某个函数的作用域链是这个样子的:

       当前函数上下文的活动变量 - 上层闭包 - 上上层闭包 .....

 

       因此闭包的概念应该是这样的:在某个函数内声明了另外一个函数(子函数),子函数(或子函数的子函数)引用了当前函数声明的一些变量或函数,这些变量或函数由于在子函数(或子函数的子函数)的作用域中会被访问到,因此这些变量或函数的访问地址被保存到了一个对象中,这个对象就是一个闭包。

  闭包的实际作用应该为子函数的函数作用域链提供一个可访问的对象,即使当前的函数已经从栈中移出,由于子函数中已经将该闭包加到了作用域链中(子函数的子函数可继承使用子函数的所有闭包),子函数所有的变量或函数都是从作用域链中获取的,因此子函数从表现上看仍旧可以访问在父函数或父父函数中声明的变量(实际上访问的是作用域链上的变量)。

  这样,函数的作用域链的规则应该是这样的:

       当前函数上下文的活动变量 - 上层函数的闭包 - ...上层函数拥有的闭包

这样一看就清晰明了了吧。

 

3. 理论验证

  为验证这个推论,建立一个带有四层函数的脚本。其中第一层中声明变量a,被second、four引用, cuse变量仅被four引用,unuse未被任何子函数引用。

(function () {
  var a = 1
  var cuse = 2
  var unuse = 10

  function second () {
    var b = a

    function third() {
      var d = 'xx'
      
      function four () {
        var c = b
        c += a
        c += cuse
        console.log(c)
      }
      four()
      console.log(d)
    }
    third()
  }

  second()

  var final = unuse + 1
  return final
})()

 

  1. 执行到second时,看到匿名函数的closure中只有a和cuse,但second中未引用匿名函数声明的任何一个变量,说明这个closure应该是在编译阶段就确定了哪些变量会被加入到closure中。

 

  2. 执行到third函数时,closure中出现了匿名函数的closure和second的closure。

  3. 执行到four时,出现了匿名函数的closure和second的closure。由于third中声明的变量没有被子函数引用,它未产生closure,在four中也就没有出现closure。

 

  从上面的结果看出:编译阶段已经确认了哪些变量会被加入到closure中,这个closure会被加入到所有子函数的作用域链中。换种理解就是子函数的使用域链中closure构成是由上层函数生成的closure以及它所拥有的所有closure组成的。

 

  当使用eval函数时,会有更明显的对比(见下图):使用eval后,父函数声明的所有变量和函数都被加入到closure中。这是因为eval执行具有不确定性,为保证变量可以访问,只能将所有变量加入closure中,避免代码执行过程中找不到变量的问题(arguments之所以被加入到closure中,是因为新的箭头函数导致子函数会使用父函数的arguments对象)。

 

4. 总结

  闭包的概念还是比较难以理解的,要从浏览器解释执行代码的角度理解才会更深刻(貌似这不是写代码的考虑的,不知道为啥面试全会问)。感谢·这波能反杀·博主的详细讲解,一对比文章质量,我写的估计只有理解的人能快速看明白,有点像写论文,但写的匆忙质量差,还是要多练习。继续加油吧~~

 

附: 博文地址 https://yangbo5207.github.io/wutongluo/ji-chu-jin-jie-xi-lie/si-3001-zuo-yong-yu-lian-yu-bi-bao.html

 

posted @ 2017-12-15 17:35  无梦灬  阅读(774)  评论(4编辑  收藏  举报