深入理解JS之执行上下文

1,089 阅读7分钟

Web环境中的JS主要通过浏览器的JS解释器解释执行代码,这是宏观层面的事实。而就js语言的具体实现上,js代码是执行在执行上下文中,也叫做执行环境。本文主要介绍JS的执行上下文,首先普及相关概念。

关键字:VO、执行上下文(执行环境EC)、执行栈、AO、scope chain、变量声明提升

  • EC:函数执行环境或执行上下文,全称是Execution Context
  • ECS:执行环境栈,全称Execution Context Stack
  • VO:变量对象,全称Variable Object
  • AO:活动对象,全称Active Object
  • scope chain:作用域链

变量对象(Variable Object)

变量对象是一个特殊的对象,并且与执行上下文息息相关,VO里面会存有下列内容:

  • variables(var,variableDeclaration,arguments)---声明变量
  • function declarations(FD)---声明函数
  • function formal parameters---接收参数

VO是说JS的执行上下文中都有个对象用来存放执行上下文中可被访问但是不能被delete的函数标示符、形参、变量声明等。它们会被挂在这个对象上,对象的属性对应它们的名字对象属性的值对应它们的值但这个对象是规范上或者说是引擎实现上的不可在JS环境中访问到活动对象。

简单来说,VO中会保存当前环境中声明、定义、接受的各类参数,但是VO是浏览器引擎的具体实现方案,js语言是无法访问到的。

激活对象(Activation object)

有了变量对象存每个上下文中的东西,但是它什么时候能被访问到呢?就是每进入一个执行上下文时,这个执行上下文儿中的变量对象就被激活,也就是该上下文中的函数标示符、形参、变量声明等就可以被访问到了。

注意点:

  • 只有全局上下文中的变量对象允许通过VO的属性名称间接访问(全局对象自身就是变量对象),其他的必须要先激活后访问
  • 对于其他的上下文直接去引用变量对象是不可能的,纯粹是一种内部的实现机制
  • 虽然我们编写的代码无法访问到这个对象,但解析器还处理数据的时候会在后台使用它
  • VO不是指具体某个Object,而是指一类Object,所以也具有一定程度的抽象
  • 在全局作用域就是全局对象,而在其他作用域是活动对象AO。Activation object(AO)
  • 其实AO是VO的一种情况.全局下是没有arguments这个对象的,所以全局对象不能称为活动对象。
  • VO是JS Engine内部实现,用于identifier resolution,JS代码层面是接触不到的。

全局对象在被创建的时候会初始化包含下列属性,例如Math,String,Date等等。同样的也会创建一个额外的对象指向自身,例如在浏览器对象模型中,会创建一个window对象并且指向global object、在node.js中就是指的global对象。

global = {
    Math: <...>,
    String: <...>,
    .....
    .....
    window: global //平时通过window对象访问就是通过这个
}

执行上下文(执行环境)

概念: 执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行。

类型

  • 全局执行上下文: 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下,VO),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
  • 函数执行上下文: 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序执行一系列步骤。
  • Eval 函数执行上下文: 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用 eval,所以在这里不会讨论。

执行栈 / 执行上下文栈(Execution Context Stack)

执行栈,也就是在其它编程语言中所说的“调用栈”,是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。 当 JavaScript 引擎第一次遇到你的脚本时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。 引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。

简单来说,函数多了,就有多个函数执行上下文,每次调用函数创建一个新的执行上下文,那如何管理创建的那么多执行上下文呢?JavaScript 引擎创建了执行上下文栈来管理执行上下文。

执行上下文和执行栈的渊源

JavaScript执行在单线程上,所有的代码都是排队执行。一开始浏览器执行全局的代码时,首先创建全局的执行上下文,压入执行栈的顶部。每当进入一个函数的执行就会创建函数的执行上下文,并且把它压入执行栈的顶部。当前函数执行完成后,当前函数的执行上下文出栈,并等待垃圾回收。浏览器的JS执行引擎总是访问栈顶的执行上下文。全局上下文只有唯一的一个,它在浏览器关闭时出栈。因为JS引擎创建了很多的执行上下文,所以JS引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文。

引用网上的一个实例:

let a = 'Hello World!';
function first() {
  console.log('Inside first function');
  second();
  console.log('Again inside first function');
}
function second() {
  console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');

当上述代码在浏览器加载时,JavaScript 引擎创建了一个全局执行上下文并把它压入当前执行栈。当遇到 first() 函数调用时,JavaScript 引擎为该函数创建一个新的执行上下文并把它压入当前执行栈的顶部。 当从 first() 函数内部调用 second() 函数时,JavaScript 引擎为 second() 函数创建了一个新的执行上下文并把它压入当前执行栈的顶部。当 second() 函数执行完毕,它的执行上下文会从当前栈弹出,并且控制流程到达下一个执行上下文,即 first() 函数的执行上下文。 当 first() 执行完毕,它的执行上下文从栈弹出,控制流程到达全局执行上下文。一旦所有代码执行完毕,JavaScript 引擎从当前栈中移除全局执行上下文。

简单表示执行上下文

说了这么多相关概念,EC,SC以及VO这三者们具体是什么关系?下面用伪代码和一张网图简单表示一下:

伪代码:

ExecutionContext(执行环境) = {
   variableObject: { .... },(变量对象)
   this: thisValue,
   Scope: [ // Scope chain(作用域链)
     // 所有变量对象的列表
   ]
};

其中VO是抽象概念,具体来讲又有变量环境和词法环境的具体实现,图片如下:

图片中的outer指向外层环境的引用,具体再下篇文章中说明。