JavaScript深入之执行上下文

可执行代码

javascript的可执行代码(executable code)的类型
全局代码 函数代码 eval代码

执行上下文栈

JavaScript引擎创建了执行上下文栈(Execution context stack ,ECS) 来管理执行上下文。
对于执行上下文,都有三个重要属性:

  1. 变量对象(Variable object,VO)
  2. 作用域链(Scope chain)
  3. this

    变量对象

    变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。

    全局上下文

    全局对象JavaScript代码中,作为JavaScript的全局函数和全局属性的占位符。通过使用全局对象,可以访问所有其他对象所有预定义的对象 函数 属性。
    在顶层JavaScript代码中,可以用关键字 this引用全局对象。
    全局上下文中的变量对象就是全局对象

    函数上下文

    在函数上下文中,我们用活动对象(activation object,AO) 来表示变量对象。活动对象是在进入函数上下文时刻被创建的,它通过函数的arguments属性初始化。arguments属性值是Arguments对象。

    执行过程

    执行上下文的代码会分成两个阶段进行处理: 分析和执行

  4. 进入执行上下文

  5. 代码执行

    进入执行上下文

    当进入执行上下文时,这个时候还没有执行代码。
    变量对象会包括:

  6. 函数的所有形参(如果是函数上下文)

    • 由名称和对应值组成的一个变量对象的属性被创建
    • 没有实参,属性值设为undefined
    1. 函数声明
    • 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
    • 如果变量对象已经存在相同名称的属性,则完全替换这个属性。
    1. 变量声明
    • 由名称和对应值(undefined)组成一个变量对象的属性被创建
    • 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。

      代码执行

      在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值。
      1. AO = {
      2. arguments: {
      3. 0: 1,
      4. length: 1
      5. },
      6. a: 1,
      7. b: 3,
      8. c: reference to function c(){},
      9. d: reference to FunctionExpression "d"
      10. }

      总结

  7. 全局上下文的变量对象初始化是全局对象。

  8. 函数上下文的变量对象初始化只包括Arguments对象
  9. 在进入执行上下文时会给变量对象添加形参 函数声明 变量声明 等初始化的属性值。
  10. 在代码执行阶段,会再次修改变量对象的属性值。

    作用域链

    当查找变量对象的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

    函数创建

    函数的作用域在函数定义的时候就决定了。词法作用域
    函数有个内部属性[[scope]],当函数创建的时候,就会保存所有父变量对象到其中,可以理解为[[scope]]就是所有父变量对象的层级链,但是注意 [[scope]]并不代表完整的作用域链!。 ```

function foo() { function bar() { … } }

  1. 函数创建时,各自的[[scope]]为

foo.[[scope]] = [ globalContext.VO ];

bar.[[scope]] = [ fooContext.AO, globalContext.VO ];

  1. <a name="wH0as"></a>
  2. ### 函数激活
  3. 当函数激活时,进入函数上下文,创建VO/AO后,就会将活动对象添加到作用域的前端。

Scope = [AO].concat([[Scope]]);

  1. <a name="OGeOx"></a>
  2. ## 捋一捋

var scope = “global scope”; function checkscope(){ var scope2 = ‘local scope’; return scope2; } checkscope();

  1. 1. checkscope()函数被创建,保存作用域链到内部属性[[scope]]

checkscope.[[scope]] = [ globalContext.VO ];

  1. 2. 执行checkscope函数,创建checkscope函数执行上下文,并把checkscope函数压入栈中

ECStack = [ checkscopeContext, globalContext ];

  1. 3. checkscope函数并不会立即执行,开始准备工作,复制函数 [[scope]]属性创建作用域

checkscopeContext = { Scope: checkscope.[[scope]], }

  1. 4. 第二步 arguments创建活动对象,随后初始化活动对象, 加入形参 函数声明 变量声明

checkscopeContext = { AO: { arguments: { length: 0 }, scope2: undefined }, Scope: checkscope.[[scope]], }

  1. 5. 第三步:将活动对象压入 checkscope 作用域链顶端

checkscopeContext = { AO: { arguments: { length: 0 }, scope2: undefined }, Scope: [AO, [[Scope]]] }

  1. 6. 准备工作做完,开始执行函数,随着函数的执行,修改AO的属性值

checkscopeContext = { AO: { arguments: { length: 0 }, scope2: ‘local scope’ }, Scope: [AO, [[Scope]]] }

  1. 7. 查找到scope2的值,返回后函数执行完毕,函数上下文从执行上下文栈种弹出。

ECStack = [ globalContext ]; ```

js代码运行的各个阶段

在V8引擎中js代码的运行过程主要分成三个阶段

  1. 语法分析阶段。该阶段会对代码进行语法分析,检查是否有语法错误(SyntaxError),如果发现语法错误,会在控制台抛出异常并终止执行。
  2. 编译阶段 该阶段会进行执行上下文(Execution Context)的创建,包括创建变量对象、建立作用域链、确定 this 的指向等。每进入一个不同的运行环境时,V8 引擎都会创建一个新的执行上下文。
  3. 执行阶段 将编译阶段中创建的执行上下文压入调用栈,并成为正在运行的执行上下文,代码执行结束后,将其弹出调用栈。

    执行上下文创建

    js运行环境包括全局环境 函数环境 eval环境,其中全局环境和函数环境创建过程如下

  4. 第一次载入 JavaScript 代码时,首先会创建一个全局环境。全局环境位于最外层,直到应用程序退出后(例如关闭浏览器和网页)才会被销毁。

  5. 每个函数都有自己的运行环境,当函数被调用时,则会进入该函数的运行环境。当该环境中的代码被全部执行完毕后,该环境会被销毁。不同的函数运行环境不一样,即使是同一个函数,在被多次调用时也会创建多个不同的函数环境。
  6. 每进入一个不同的运行环境时,JavaScript 都会创建一个新的执行上下文,该过程包括:
  • 建立作用域链(Scope Chain);
  • 创建变量对象(Variable Object,简称 VO);

  • 确定 this 的指向。

    创建变量对象

    变量对象,每个执行上下文都会有一个关联的变量对象,该对象上会保存这个上下文中定义的所有变量和函数。
    而在浏览器中,全局环境的变量对象是window对象,因此所有的全局变量和函数都是作为window对象的属性和方法创建的。相应的,在 Node 中全局环境的变量对象则是global对象。
    在 JavaScript 中,词法环境又分为词法环境(Lexical Environment)和变量环境(Variable Environment)两种,其中:

  • 变量环境用来记录var/function等变量声明;

  • 词法环境是用来记录let/const/class等变量声明。

通过使用两个词法环境(而不是一个)分别记录不同的变量声明内容,JavaScript 实现了支持块级作用域的同时,不影响原有的变量声明和函数声明。

建立作用域链

作用域就是词法环境,而词法环境由两个成员组成。

  • 环境记录(Environment Record):用于记录自身词法环境中的变量对象。
  • 外部词法环境引用(Outer Lexical Environment):记录外层词法环境的引用。

通过外部词法环境的引用,作用域可以层层拓展,建立起从里到外延伸的一条作用域链。当某个变量无法在自身词法环境记录中找到时,可以根据外部词法环境引用向外层进行寻找,直到最外层的词法环境中外部词法环境引用为null,这便是作用域链的变量查询。
在执行阶段,变量对象(VO)会被激活为活动对象(AO),变量会进行赋值,此时活动对象才可被访问。在执行结束之后,作用域链和活动对象均被销毁,使用闭包可使活动对象依然被保留在内存中。这就是 JavaScript 代码的运行过程