纵横研究院前端基础技术专题社区

【原创】Javascript-执行上下文

2019-04-08  本文已影响0人  whelm

在 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 管理作用域

上一篇下一篇

猜你喜欢

热点阅读