深入剖析函数执行流程

  本篇文章,对JavaScript函数的执行流程做了细致入微的分析。话不多说,我以下面这个例子作为样例——

 1 (function ($){
 2     
 3     function foo() {
 4         var x = 10;
 5         return function bar() {
 6             console.log(x);
 7         };
 8     }
 9  
10     var f = foo();
11  
12     var x = 20;
13  
14     f(); // 结果是10而不是20
15 
16 })(jQuery)

  对于它的执行过程,可用一张图来表示,如下——

                      函数执行流程图解

  对于该图的说明如下——

  1 /**
  2   * 首先需要明白函数执行流控制机制:
  3   *  1>进入函数具有两个步骤:
  4   *      a>进入函数执行环境
  5   *         注意:在进入执行环境之前,会创建有关该执行环境的活动对象对象AO,主要包括三个内容——
  6   *         i>函数参数
  7   *         ii>该执行环境内的所有函数声明
  8   *         iii>该执行环境内的所有变量声明
  9   *     b>执行代码
 10   *         注意:只有在代码执行的时候才对声明了的变量进行赋值操作。
 11   *  2>每个函数在被调用时都会创建自己的执行环境,接着将函数的执行环境推入环境栈中。
 12   *    在函数执行完之后,将控制权返回给之前的执行环境。是典型的堆栈控制方式。
 13   *  3>从这个角度来讲,一旦刚开始执行JS代码,就可以理解为调用了这个全局函数。紧接着,做了一个调用普通函数该做的事情。
 14   */
 15  
 16  /**
 17   * 这个匿名函数是第一个匿名函数,在这里称之为Anonymous Function 1,简称AF1
 18   * 当AF1被调用时,会出现3个过程:
 19   *     1>创建该函数,创建该函数的包含全局变量对象的[[scope]]属性;
 20   *     2>调用函数后,创建AF1执行环境,并压入执行环境堆栈中,此时的ECStack = [ AF1Context, globalContext ];
 21   *       此时的执行环境包括3个内容:
 22   *         a>this指针:Window;需要注意的是,this指针是包含在变量(活动)对象里的。
 23   *         b>作用域链Scope:这里的Scope = AF1.Context.Ao + AF1.[[scope]]
 24   *                                     = AF1.Context.Ao + globalContext.VO;
 25   *           注意:这里的作用域链本质上是一个指向变量对象的列表,它只引用,但不包含变量对象。
 26   *                这里的[[scope]]属性是所有父级变量对象的层级链,对于下面的[[scope]]属性皆为如此。
 27   *                对于变量对象和活动对象,在具体实现层面上只是一个抽象概念。
 28   *         c>活动对象AO:这里的AO = {
 29   *                                     foo: <reference to function>,
 30   *                                     f  : undefined,
 31   *                                     x  : undefined
 32   *                                 }
 33   *           注意:这里的f和x值是在执行函数的时候赋值的。
 34   *     3>接着就是执行AF1内部代码了。
 35   */
 36  (function ($){
 37      
 38      function foo() {
 39          /**
 40           * 1>创建foo函数的执行环境,并压入执行环境栈中,此时的ECStack = [ fooContext, AF1Context, globalContext ];
 41           * 2>这个函数的作用域链scope = [ fooContext.AO + AF1Context.Ao + globalContext.VO]
 42           */
 43          var x = 10;  //这句执行完成后,这个x的值变为10,而且这个变量x是属于fooContext.AO的。
 44            
 45          //这里的匿名函数是第二个匿名函数,在这里在称之为Anonymous Function 2,简称AF2
 46          return function() {
 47              /**
 48               * 1>创建foo函数的执行环境,并压入执行环境栈中,此时的ECStack = [ AF2Context, AF1Context, globalContext ];
 49               * 2>这个函数的作用域链scope = [ fooContext.AO + AF1Context.AO + globalContext.VO ]
 50               * 3>当程序开始检索x变量时,根据作用域链的先后顺序开始查找,即先去fooContext.AO中查找,
 51               *   如果没有,就从AF1Context.AO中查找,发现找到了x变量,就停止对剩余的变量对象VO中的x变量的查找过程。
 52               *   自然就能够清晰的明白,输出结果为什么是10,而不是20。从而就能够明白静态作用域和动态作用域的区别了。
 53               */
 54              console.log(x);
 55          };  //当这个匿名函数返回后,此时的ECStack = [ AF1Context, globalContext ];
 56      }  //foo函数返回后,fooEC退出栈顶,此时的ECStack = [ AF1Context, globalContext ];
 57      
 58      /**
 59       * 这种函数调用模式被称作函数调用模式(另外还包括方法调用模式、构造器调用模式和Apply调用模式)
 60       * 当函数以此模式调用时,this被绑定到全局对象。对于this指针,作以如下说明:
 61       *     1>它是执行环境的一个属性,它以活动对象的其中一员进行呈现;
 62       *     2>它是在进入执行环境的时候被确认,并且在执行环境运行期间永久不变;
 63       *     3>它并没有类似变量向上一层一层搜索的过程,直接从执行环境中获取;
 64       *     4>它只能获取,不能赋值。
 65       *     5>它是由激活对应函数执行环境的调用者来提供的,即调用函数的父执行环境;
 66       *       调用函数的方式影响了对应函数执行环境的this值。
 67       * 执行完这句代码,开始调用foo函数。
 68       */
 69      var f = foo();  
 70       
 71      var x = 20;  //执行完x = 20;这句代码后,也就说明AF1的活动对象的赋值就完成了。
 72       
 73      f(); // 开始调用AF2
 74      
 75  })(jQuery)  //当这个函数返回后,此时的ECStack = [ globalContext ];
 76  
 77  /**
 78   * 从这个例子可以看出,执行环境栈ECStack是如何工作的,ECStack一直保存着全局执行环境globalContext
 79   * 对于全局执行环境,以下做以几点说明——
 80   *     1>当碰见可执行代码时,便进入执行环境,全局执行环境是最外围执行环境
 81   *     2>何时消亡:退出应用程序——关闭网页或浏览器
 82   *     3>活动的执行环境组逻辑上组成一个堆栈,堆栈底部永远是全局执行环境
 83   *     4>在进入全局执行环境之前,会创建全局变量对象,这个对象只存在一份,它的属性在任何地方都可以访问
 84   */
 85  
 86  /**
 87   * -----------------------------------------
 88   * 参考资料:
 89   *         汤姆大叔博客——
 90   *             JavaScript核心
 91   *             执行上下文
 92   *             变量对象
 93   *             this指针
 94   *             作用域链
 95   *             函数(Functions)
 96   *             闭包(Closures)
 97   *         JavaScript高级程序设计(第二版)——
 98   *             第四章:变量、作用域和内存问题
 99   *             第七章:匿名函数
100   *         JavaScript语言精粹——
101   *             第四章:函数
102   * -----------------------------------------
103   * 
104   */

  理解这个例子的执行流程,相信就会深入的理解下面几个关键词(概念)——

    1>静态作用域、动态作用域

    2>闭包——共享作用域和私有作用域

    3>标识符解析

    4>变量对象、活动对象

    5>函数生命周期

  举一反三,任何JavaScript代码的执行,均能做以细致分析,每执行一句代码,我们就能知道下面几点要素——

    1>变量或函数是否已赋值

    2>哪些变量或函数已经被销毁,哪些依旧存在于内存中

    3>当前执行环境的this指针是什么

    4>当前执行环境能够访问到哪些变量

  知道了函数的执行流程,再去深入理解闭包和模块模式,就会变得非常清晰了。

    

  

原文地址:https://www.cnblogs.com/jinguangguo/p/2635407.html