执行上下文、执行栈、作用域链、闭包,这其实是一整套相关的东西,之前转载的文章也有讲到这些。下面两篇文章会更加详细地解释这些概念。

1.执行上下文

1.1 定义

执行上下文(execution context)是当前 JavaScript 代码被解析和执行时所在环境的抽象概念。

1.2 类型

  • 全局执行上下文
    只有一个。它创建了一个全局对象(浏览器中是window对象),并将this指向该对象。
  • 函数执行上下文
    无数个。每次调用函数时,都会为该函数创建一个新的执行上下文。
  • eval 函数执行上下文
    运行在 eval 函数中的代码也获得了自己的执行上下文,eval 函数不常用,所以这里不讨论

2.执行栈

执行栈(execution stack),也即调用栈(call stack),具有 LIFO(后进先出)结构,用于存储在代码执行期间创建的所有执行上下文。
当 JavaScript 引擎首次读取脚本时,它会创建一个全局执行上下文并将其 push 到当前的执行栈。每当调用函数的时候,都会为该函数创建一个新的执行上下文并将其 push 到栈顶;在函数执行完毕后,对应的执行上下文将会从栈顶 pop 出,上下文控制权将移到当前执行栈的下一个执行上下文。

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');

// Inside first function
// Inside second function
// Again inside first function
// Inside Global Execution Context

3.执行上下文的创建

执行上下文分两个阶段创建:

  • 创建阶段(The Creation Phase)
  • 执行阶段(The Execution Phase)

3.1 创建阶段

  • 词法环境组件被创建
  • 变量环境组件被创建

用伪代码表示就是:

ExecutionContext = {  
  LexicalEnvironment = { ... },   // 词法环境
  VariableEnvironment = { ... },  // 变量环境
}

3.1.1 词法环境

词法环境(Lexical environment)是一个包含标识符变量映射的结构。(这里的标识符表示变量/函数的名称,变量是对实际对象【包括函数类型对象】或原始值的引用)

词法环境有三个组成部分:

  • 环境记录:存储变量和函数声明的实际位置
  • 对外部环境的引用:可以访问其外部词法环境
  • this绑定:确定this的指向

词法环境有两种类型:

  • 全局环境:全局执行上下文的词法环境。
  • 函数环境:函数执行上下文的词法环境。

3.1.1.1 环境记录:

根据词法环境的两种类型,环境记录(Environment record)同样也有两种类型:

  • 对象环境记录(Object environment record):
    全局环境的环境记录类型。存储全局变量和函数声明、全局对象(window 对象)和关联的属性/方法。

  • 声明性环境记录(Declarative environment record):
    函数环境的环境记录类型。存储局部变量和函数声明、arguments对象。arguments对象包含了索引与参数之间的映射,以及传给函数的参数的个数。

    function foo(a, b) {
      var c = a + b;
    }
    foo(2, 3);
    // argument object(类数组的对象)
    Arguments: {0: 2, 1: 3, length: 2},

3.1.1.2 外部环境引用:

外部环境引用(Reference to the outer environment)表明当前词法环境能够访问外部词法环境。这意味着如果JavaScript引擎未在当前词法环境找到变量,它将向外部词法环境寻找(这有点类似原型链中的属性查找)

全局环境没有外部环境,其外部环境引用为 null。

函数环境有外部环境,其外部环境引用可以是全局环境,也可以是包含内部函数的外部函数环境。

3.1.1.3 this 绑定:

全局执行上下文中,this 绑定(this binding)到全局对象(对于浏览器,该对象为 window);函数执行上下文中,this 绑定到谁将取决于函数的调用位置(或者说调用方法)。
我会在另一篇文章总结 this 的绑定机制,所以这里不再展开。

讲完了词法环境的三个组成部分,最后再配合伪代码理解一下:

// 全局执行上下文
GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 标识符绑定在这里 
    }
    outer: <null>,
    this: <global object>
  }
}

// 函数执行上下文
FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 标识符绑定在这里 
    }
    outer: <Global or outer function environment reference>,
    this: <depends on how function is called>
  }
}

3.1.2 变量环境

变量环境(Variable environment)同样也是词法环境,因此它具有上面定义的词法环境的所有特征。这两者的区别主要在于:
在 ES6 中,词法环境用于存储函数声明和 let/const 变量绑定,而变量环境仅用于存储 var 变量绑定。

3.2 执行阶段

在执行阶段,完成对所有变量的分配,最后执行代码。

3.3 举例说明

通过一个例子来了解执行上下文的整个创建和执行过程。
以下面的代码为例

let a = 20;
const b = 30;
var c;
function multiply(e, f) {
 var g = 20;
 return e * f * g;
}
c = multiply(20, 30);

在开始读取代码后,JavaScript 引擎创建全局执行上下文并压栈,全局执行上下文的创建阶段的伪代码如下:

GlobalExectionContext = {
  // 词法环境
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 标识符绑定在这里
      a: < uninitialized >,
      b: < uninitialized >,
      multiply: < func >
    }
    outer: <null>,
    ThisBinding: <Global Object>
  },

  // 变量环境
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 标识符绑定在这里
      c: undefined,
    }
    outer: <null>,
    ThisBinding: <Global Object>
  }
}

之后进入全局执行上下文的执行阶段,开始进行变量分配/赋值,伪代码如下:

GlobalExectionContext = {
    // 词法环境
    LexicalEnvironment: {
        EnvironmentRecord: {
            Type: "Object",
            // 标识符绑定在这里
            a: 20,
            b: 30,
            multiply: < func >
        }
        outer: <null>,
        ThisBinding: <Global Object>
    },

    // 变量环境
    VariableEnvironment: {
        EnvironmentRecord: {
            Type: "Object",
            // 标识符绑定在这里
            c: undefined,
        }
        outer: <null>,
        ThisBinding: <Global Object>
    }
}

随着执行阶段的进行,我们遇到了multiply(20, 30),这是一个函数调用语句,所以此时创建了该函数对应的函数执行上下文并压栈,函数执行上下文的创建阶段的伪代码如下:

FunctionExectionContext = {
    // 词法环境
    LexicalEnvironment: {
        EnvironmentRecord: {
            Type: "Declarative",
            // 标识符绑定在这里
            Arguments: {0: 20, 1: 30, length: 2},
        },
        outer: <GlobalLexicalEnvironment>,
        ThisBinding: <Global Object or undefined>,
    },

    // 变量环境
    VariableEnvironment: {
        EnvironmentRecord: {
            Type: "Declarative",
            // 标识符绑定在这里
            g: undefined
        },
        outer: <GlobalLexicalEnvironment>,
        ThisBinding: <Global Object or undefined>
    }
}

之后进入函数执行上下文的执行阶段,开始进行函数内的变量的分配/赋值,伪代码如下:

FunctionExectionContext = {
    // 词法环境
    LexicalEnvironment: {
        EnvironmentRecord: {
            Type: "Declarative",
            // 标识符绑定在这里
            Arguments: {0: 20, 1: 30, length: 2},
        },
        outer: <GlobalLexicalEnvironment>,
        ThisBinding: <Global Object or undefined>,
    },

    // 变量环境
    VariableEnvironment: {
        EnvironmentRecord: {
            Type: "Declarative",
            // 标识符绑定在这里
            g: 20
        },
        outer: <GlobalLexicalEnvironment>,
        ThisBinding: <Global Object or undefined>
    }
}

函数执行完毕,函数执行上下文出栈,此时的执行上下文是全局执行上下文。由于函数的返回值被赋给变量 c,此时全局执行上下文对应的全局词法环境得到更新,伪代码如下:

GlobalExectionContext = {
    // 词法环境
    LexicalEnvironment: {
        EnvironmentRecord: {
            Type: "Object",
            // 标识符绑定在这里
            a: 20,
            b: 30,
            multiply: <func>
        }
        outer: <null>,
        ThisBinding: <Global Object>
    },

    // 变量环境
    VariableEnvironment: {
        EnvironmentRecord: {
            Type: "Object",
            // 标识符绑定在这里
            c: 12000,
        }
        outer: <null>,
        ThisBinding: <Global Object>
    }
}

全局执行上下文的执行阶段结束,程序结束。

4. 再看变量提升

在上面全局执行上下文创建阶段的伪代码中我们可以看到,letconst定义的变量没有任何与之关联的值,处于一个未初始化的状态,但var定义的变量设置为undefined

这是因为在创建阶段,JavaScript 引擎会扫描一遍代码并解析所有的变量和函数声明,其中函数声明被存储在环境记录中,而变量的情况则比较特殊:var声明的变量将被设置为undefinedletconst声明的变量将保持未初始化。

因此,我们可以在声明之前就访问var定义的变量,尽管是undefined ;但如果在声明之前访问letconst定义的变量则会提示引用错误,因为在执行阶段之前其始终是未初始化的,处于 TDZ 暂时性死区。

这就是我们所谓的变量提升。

注: 在执行阶段,如果Javascript引擎在源代码中声明的实际位置找不到 let变量的值,那么将为其分配undefined值。

5. 补充:关于 this binding

本文参考自:

如果你发现译文和原文在 this binding 的说法上存在出入,例如:
在原文中:

The execution context is created during the creation phase. Following things happen during the creation phase:
1.LexicalEnvironment component is created.
2.VariableEnvironment component is created.

Each Lexical Environment has three components:
1.Environment Record
2.Reference to the outer environment,
3.This binding

在译文中:

在任何 JavaScript 代码执行之前,执行环境经历了创建阶段,创建阶段包含以下三个事:
1.this 的值确定,也被称为 This Binding.
2.Lexical Environment 被创建。
3.Variable Environment 被创建。

在词法环境中,有两种组件:
(1) environment record
(2) reference to the outer environment.

下面这篇文章的评论区进行了解释:

总而言之是由于 ECAMAScript 的标准变更导致的。原文最初是基于 ES5 编写的,this 绑定的确是执行上下文创建阶段的一环,但是在 ES2018 的规范中,this 绑定被并入词法环境的环境记录,所以原作者后来进行了更改。