zoukankan      html  css  js  c++  java
  • 深入剖析函数执行流程

      本篇文章,对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>当前执行环境能够访问到哪些变量

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

        

      

  • 相关阅读:
    STM8s在利用库配置端口的小问题
    ABAP调试
    READ TABLE 的用法
    人在低谷
    力扣 两数之和
    未来选择
    选择
    室友问题该如何解决呢?
    力扣 两数之和
    谈谈自己
  • 原文地址:https://www.cnblogs.com/jinguangguo/p/2635407.html
Copyright © 2011-2022 走看看