【原创】Javascript-执行上下文

作者: whelm | 来源:发表于2019-04-08 12:29 被阅读0次

    在 javascript中每一个函数在执行时都会创建一个独属于这个函数的执行上下文,其中存储了该函数创建的,可访问的变量,函数内部 this 指向等等。在函数执行完毕之后执行上下文会被销毁。

    本文分析函数创建到被调用结束,执行上下文被创建,压入栈,之后被销毁这个过程,由此引出函数作用域,变量对象等概念。该文章中大部分代码为伪代码,只是意图说明某种结构或者概念。

    全局环境

    Javascript 中当代码开始执行时,会首先创建一个全局的执行上下文,全局环境中的变量都存储在该执行上下文中。同时为了便于管理,这个全局的执行上下文会被推入执行上下文栈。在 javascript 运行过程中,全局执行上下文始终保持在栈底,直到程序运行结束,才会被推出并销毁。

    函数环境

    当一个被声明的函数被调用时,会创建独属于这个函数的执行上下文,并且将该执行上下文推入执行上下文栈,函数调用结束后,函数对应的执行上下文被推出并销毁。要注意的是在这个函数中如果调用了另外的函数B,则又会创建这个函数 B 的执行上下文,并推入栈。

    个人理解,执行上下文相当于一个函数执行的环境,javascript 将其存储在执行上下文栈中,在这个环境中保存了该函数可以访问的变量,以供函数运行时调用。而如果我们在函数中不断地创建新的函数,调用函数,则执行上下文栈会不断被推入上下文,如果调用次数过多,超过了堆栈可容纳的大小,则会造成堆栈溢出。这就是为什么有些递归次数过多的程序会发生堆栈溢出的问题。

    执行上下文

    那么执行上下文中究包含一些什么东西呢?

    执行上下文中主要包含:

    context: {
        VO,      // 变量对象
        scope,  // 函数作用域链
        this,   // 函数 this 指向
    }
    

    下面我们对这里前两项的内容进行详细的解释。由于我们判断 this 指向问题,一般由调用时判断,与执行上下文创建过程关系不大,因此本文暂不对 this 进行解释,后续会单独出 this 相关的文章。

    VO

    指变量对象(Variable Object),其中存储函数内部声明的一些变量,方法,函数的参数等。
    与变量对象相对的是活动对象(Activation Object)。这两者其实本质是一样的,都是用以存储函数内部属性。区别在于,在函数预编译过程中,就是函数被调用的准备阶段,这个对象是变量对象,其属性不可访问;在函数执行阶段,该对象变为活动对象,其上的属性可以进行访问。

    变量对象的内容一般如下:

    VO = {
       arguments: {},  // 参数
       a,  // 声明的变量
       c: reference to function c,  // 声明的函数
    }
    

    例如,对于

    function a(x) {
       var y = 20;
       var  w = function(){ ... }
       function  z(){ ... }
    }
    a(2);
    

    在 a 函数运行的准备阶段,它的执行上下文的 VO 是:

    VO = {
       arguments: {
          0: 2,
          length: 1,
       },
       x: 2,
       y: undefined,
       w: undefined,
       z: reference to function z,
    }
    

    这里有一个规则,在一个函数被调用,它的执行上下文的创建阶段,对应 VO 的创建阶段。相当于函数还未运行时 VO 的初始化,这个过程中,顺序 ”运行“ 函数内的代码,但是仅进行变量的声明而非赋值,对于参数和函数声明例外。

    如上图代码所示,参数 x 及函数 z 此时已经有了值,而 y 变量和使用变量声明的函数 w ,则为 undefined.

    要注意,如果该函数调用时未传入实参,则 x 应为 undefined.

    OK, 当函数准备完执行前该准备的一切,此时进入执行阶段,VO 变为 AO,并且函数真正意义上地开始顺序执行。这个过程包括执行输出语句,对进行了赋值的变量依次填充内容,调用函数内部声明的函数,进而创建另外一个执行上下文等等。

    AO = {
       arguments: {
          0: 2,
          length: 1,
       },
       x: 2,
       y: 20,
       w: reference to FunctionExpression "d",
       z: reference to function z,
    }
    

    函数从调用的准备阶段到真正的执行阶段,函数执行上下文中 VO的创建与执行,这个过程可以解释一些问题,例如变量提升,函数提升等。

    scope

    指作用域链,一个函数在运行期间,执行上下文的作用域链 = 函数外部环境变量作用域链 + 自身 AO

    在函数创建时,这个函数的 [[scope]] 属性被创建,这个属性包含了它被创建时的外部环境作用域链,例如在全局环境中创建一个函数:

    function t() { ... }
    

    在它创建时,它的[[scope]]包含了全局环境作用域,实际上每个环境的作用域由它的变量对象体现。因此

    t.[[scope]] = {
       globalContext.VO
    }
    

    如果此时运行t函数,则开始上面讲过的执行上下文,VO 创建过程,同时在 t 函数运行前的准备阶段,将会复制它的[[scope]]用于创建作用域链,并将当前的 VO/AO 推入作用域链顶层,相当于:

    scope = {
      t.AO,
      t.[[scope]],
    }
    

    相当于:

    tContext = {
        AO: ...,
        scope = {
          tContext.AO,
          globalContext.VO,
       }
    }
    
    

    同理,如果 t 函数中创建一个 f 函数,该函数的[[scope]]将包含 t 函数环境的作用域链:

    function t(){
      function f(){ ... }
    }
    
    f.[[scope]] = {
       tContext.VO,
       globalContext.VO,
    }
    

    在 f 函数运行时,再将自己的 VO/AO 推入顶层

    fContext = {
       AO: ...,
       scope = {
          fContext.AO,
          tContext.VO,
          globalContext.VO,
      }
    }
    

    这样就解释了函数作用域链的问题,函数中调用的变量,会在当前最顶层的自己的作用域中寻找,如果没有的话沿着作用域链向上进行查找,可以一直找到全局作用域。也正因为只有函数的创建和执行会进行这样的过程,因此 Javascript 中只有函数作用域,而没有块作用域。

    概念解释到目前已经完成,下面我们针对两道题目来进行分析。

    题目一
    var x = 21;
    var talk = function () {
        console.log(x);
        var x = 20;
    };
    talk (); // undefined
    

    该题目可以用变量提升来解释,我们使用上面执行上下文创建这个过程来演示一遍。

    首先在全局环境中声明 x 变量并赋值,声明 talk 变量并将一个函数的引用赋予该变量。talk 函数在创建时,其[[scope]]属性包含了 talk 函数声明时的环境作用域链,当前仅有全局作用域:

    talk.[[scope]] = {
       globalContext,
    }
    

    talk 被调用时,有一个函数执行前准备阶段,该阶段创建 talk的执行上下文,并将其压入执行上下文栈顶部,其中包含了 VO, 和从 talk.[[scope]] 复制的作用域链,并且复制作用域链之后,将自身VO 推入链顶部。

    在函数执行的准备阶段,对函数中声明的变量进行声明工作,不进行赋值。

    准备阶段结束后函数上下文主要内容如下:

    talkContext = {
        VO: {
           arguments: {
                length: 0,
           }
           x: undefined
        }
        scope: {
           VO,
           globalContext,
        }
    }
    

    开始执行 talk 函数:进行变量赋值等其他操作,顺序执行。
    执行到console.log(x);行时,由于此时 talkContext 中 x 值为 undefined,所以输出 undefined。
    输出后继续执行,x 被赋值为 20,执行上下文:

    talkContext = {
        VO: {
           arguments: {
                length: 0,
           }
           x: 20
        }
        scope: {
           VO,
           globalContext,
        }
    }
    

    函数执行完毕,talkContext 从上下文栈中被推出,并销毁。

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

    该题目可以使用静态作用域来解释,即 javascript 中的函数作用域为静态作用域,在函数创建时决定。同样,也可以使用之前的执行上下文中的作用域链来解释。

    分析一下,在 foo 函数创建时:

    foo.[[scope]] = {
       globalContext
    }
    

    bar 函数的创建和其执行上下文的创建过程我不再一一列出,我们关心 foo 函数被调用时,发生了什么。

    foo 函数执行前准备时,创建了它的执行上下文,并推入了栈中:

    fooContext = {
      VO: {
           arguments: {
                length: 0,
           }
      },
      scope: {
          VO,
          globalContext,
      }
    }
    

    foo 函数的作用域链中,除了自己的 VO,就是从[[scope]]属性复制的创建时环境中作用域链,因此,只有 VO 和 globalContext,打印 value 时,延作用域链向上寻找,第一层是当前作用域 VO,没有声明 value 变量,继续向上查找,第二层是全局作用域,定义 value 值为 1,进行打印。

    总结

    理清 Javascript 运行时执行上下文的创建,初始化和执行这个过程有助于我们理解代码实际的运行结果。可能往往编程中会遇到一些实际执行结果和预期结果不同的状况,这种时候靠这种分析手段,我们可以更好地理解代码的运行,甚至规避一些可能出现的问题。

    本文主要是分析了执行上下文的创建和销毁中间的过程,提到了执行上下文中最主要的部分,即:作用域链变量对象,以及他们的用途。

    在执行上下文中另外有记录当前函数内 this 的指向,这一概念我们会再后续的文章中进行总结。另外,Javascript 有延长作用域的功能,以起到简化代码的功能;还有闭包的产生也打破了函数内部的变量只能内部进行访问的规则,那么他们对于执行上下文究竟有什么影响,这一部分后续我们也将会有文章来进行分析和说明。

    本文参考资源如下:

    JavaScript高级程序设计(第3版)4.2 执行环境及作用域
    冴羽的博客 JavaScript深入之执行上下文
    高性能 javascript 2.1 管理作用域

    相关文章

      网友评论

        本文标题:【原创】Javascript-执行上下文

        本文链接:https://www.haomeiwen.com/subject/rwmgbqtx.html