# JavaScript执行上下文

在这篇文章中,我将深入探讨JavaScript中一个最基本的部分,即Execution Context。 在本文结束时,您应该更清楚地知道解释器是怎么工作的,为什么某些函数/变量在声明之前就可以使用以及它们的值是如何确定的。

# 1、什么是执行上下文

当Javascript代码运行的时候,所处在当前运行时的环境,就是执行上下文

通俗一点讲就是当执行到一个函数的时候,就会进行准备工作,这里的“准备工作”,就叫做“执行上下文(execution context)”

# 2、执行上下文的类型

JavaScript 中有三种执行上下文类型。

  • 全局执行上下文(Global):代码首次执行时候的默认环境,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
  • 函数执行上下文(Function):每当一个函数被调用时,都会为该函数创建一个新的上下文,每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序执行一系列步骤。
  • eval执行上下文(Eval):当eval函数内部的文本执行的时候

你可以拥有任意数量的函数上下文。每一次函数调用都会创建一个新的上下文,它会创建一个私有域,函数内部做出的所有声明都会放在这个私有域中,并且这些声明在当前函数作用域外无法直接访问。

// global context
var sayHello = 'hello';

function person() { // execution context
  var first = 'xiaoming',
      last = 'xiaoli';

  function firstName() { // execution context
    return first;
  }

  function lastName() { // execution context
    return last;
  }

  console.log(`${sayHello} ${firstName()} ${lastName()}`);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 3、执行栈

执行上下文栈(下文简称执行栈)也叫调用栈,执行栈用于存储代码执行期间创建的所有上下文,具有LIFO(Last In First Out后进先出,也就是先进后出)的特性。

JS代码首次运行,都会先创建一个全局执行上下文并压入到执行栈中,之后每当有函数被调用,都会创建一个新的函数执行上下文并压入栈内;由于执行栈LIFO的特性,所以可以理解为,JS代码执行完毕前在执行栈底部永远有个全局执行上下文。

function f1() {
    f2();
    console.log(1);
};

function f2() {
    f3();
    console.log(2);
};

function f3() {
    console.log(3);
};

f1(); // 3 2 1

/*
* 通过执行栈与上下文的关系来解释上述代码的执行过程,为了方便理解,
* 我们假设执行栈是一个数组,在代码执行初期一定会创建全局执行上下文并压入栈,
* 因此过程大致如下:
*/

//代码执行前创建全局执行上下文
ECStack = [globalContext];
// f1调用
ECStack.push('f1 functionContext');
// f1又调用了f2,f2执行完毕之前无法console 1
ECStack.push('f2 functionContext');
// f2又调用了f3,f3执行完毕之前无法console 2
ECStack.push('f3 functionContext');
// f3执行完毕,输出3并出栈
ECStack.pop();
// f2执行完毕,输出2并出栈
ECStack.pop();
// f1执行完毕,输出1并出栈
ECStack.pop();
// 此时执行栈中只剩下一个全局执行上下文
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

关于执行栈,有5点需要记住:

  • 单线程
  • 同步执行
  • 一个全局上下文
  • 无数的函数上下文
  • 每次函数调用都会创建一个新的执行上下文,即使是调用自身

# 4、执行上下文创建阶段

执行上下文创建有两个阶段:(1)创建阶段(2)执行阶段

# 4.1 创建阶段

js执行上下文的创建阶段主要负责三件事:

  1. this 值的决定,即我们所熟知的 This 绑定
  2. 创建词法环境组件(LexicalEnvironment)。
  3. 创建变量环境组件(VariableEnvironment)。

所以执行上下文在概念上表示如下:

ExecutionContext = {  
    // 确定this的值
    ThisBinding = <this value>,
    // 创建词法环境组件
    LexicalEnvironment = {},
    // 创建变量环境组件
    VariableEnvironment = {},
};
1
2
3
4
5
6
7
8

# 4.1.1 This 绑定:

在全局执行上下文中,this 的值指向全局对象。(在浏览器中,this引用 Window 对象)。

在函数执行上下文中,this的值取决于该函数是如何被调用的。如果它被一个引用对象调用,那么this会被设置成那个对象,否则this的值被设置为全局对象或者undefined(在严格模式下)。

js五种绑定彻底弄懂this,默认绑定、隐式绑定、显式绑定、new绑定、箭头函数绑定详解 (opens new window)

# 4.1.2 词法环境

官方es6 (opens new window) 文档把词法定义为:

词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符和具体变量和函数的关联。一个词法环境由环境记录器和一个可能的引用外部词法环境的空值组成。

词法环境是一种持有标识符—变量映射的结构。(这里的标识符指的是变量/函数的名字,而变量是对实际对象[包含函数类型对象]或原始数据的引用)。

在词法环境的内部有两个组件:(1)环境记录器(2)一个外部环境的引用

  1. 环境记录器是存储变量和函数声明的实际位置。
  2. 外部环境的引用意味着它可以访问其父级词法环境(作用域)。

词法环境有两种类型:全局词法环境函数词法环境

  • 全局词法环境(在全局执行上下文中)是没有外部环境引用的词法环境。全局环境的外部环境引用是 null。它拥有内建的 Object/Array/等、在环境记录器内的原型函数(关联全局对象,比如 window 对象)还有任何用户定义的全局变量,并且 this 的值指向全局对象。
  • 函数词法环境在函数内部用户定义的变量存储在环境记录器中。并且引用的外部环境可能是全局环境,或者任何包含此内部函数的外部函数。

环境记录器也有两种类型:

  1. 全局环境中,环境记录器是对象环境记录器,用来定义出现在全局上下文中的变量和函数的关系。
  2. 函数环境中,环境记录器是声明式环境记录器,用来存储变量、函数和参数。

对于函数环境-声明式环境记录器还包含了一个传递给函数的 arguments 对象(此对象存储索引和参数的映射)和传递给函数的参数的 length

词法环境在伪代码中看起来像这样:

// 全局环境
GlobalExectionContext = {
    // 全局词法环境
    LexicalEnvironment: {
        // 环境记录
        EnvironmentRecord: {
            Type: "Object", //类型为对象环境记录
            // 标识符绑定在这里 
        },
        outer: < null >
    }
};
// 函数环境
FunctionExectionContext = {
    // 函数词法环境
    LexicalEnvironment: {
        // 环境纪录
        EnvironmentRecord: {
            Type: "Declarative", //类型为声明性环境记录
            // 标识符绑定在这里 
        },
        outer: < Global or outerfunction environment reference >
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 4.1.3 变量环境

变量环境同样是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系,它具备词法环境所有属性。

在 ES6 中,词法环境组件和变量环境的一个不同就是前者被用来存储函数声明和变量(let 和 const)绑定,而后者只用来存储 var 变量绑定。

我们通过一串伪代码来理解它们:

let a = 20;  
const b = 30;  
var c;

function multiply(e, f) {  
 var g = 20;  
 return e * f * g;  
}

c = multiply(20, 30);

// 我们用伪代码来描述上述代码中执行上下文的创建过程:

//全局执行上下文
GlobalExectionContext = {
    // this绑定为全局对象
    ThisBinding: <Global Object>,
    // 词法环境
    LexicalEnvironment: {  
        //环境记录
      EnvironmentRecord: {  
        Type: "Object",  // 对象环境记录
        // 标识符绑定在这里 let const创建的变量a b在这
        a: < uninitialized >,  
        b: < uninitialized >,  
        multiply: < func >  
      }
      // 全局环境外部环境引入为null
      outer: <null>  
    },
    // 变量环境
    VariableEnvironment: {  
      EnvironmentRecord: {  
        Type: "Object",  // 对象环境记录
        // 标识符绑定在这里  var创建的c在这
        c: undefined,  
      }
      // 全局环境外部环境引入为null
      outer: <null>  
    }  
  }

  // 函数执行上下文
  FunctionExectionContext = {
     //由于函数是默认调用 this绑定同样是全局对象
    ThisBinding: <Global Object>,
    // 词法环境
    LexicalEnvironment: {  
      EnvironmentRecord: {  
        Type: "Declarative",  // 声明性环境记录
        // 标识符绑定在这里  arguments对象在这
        Arguments: {0: 20, 1: 30, length: 2},  
      },  
      // 外部环境引入记录为</Global>
      outer: <GlobalEnvironment>  
    },
    // 变量环境
    VariableEnvironment: {  
      EnvironmentRecord: {  
        Type: "Declarative",  // 声明性环境记录
        // 标识符绑定在这里  var创建的g在这
        g: undefined  
      },  
      // 外部环境引入记录为</Global>
      outer: <GlobalEnvironment>  
    }  
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

可能你已经注意到,在创建阶段,letconst 定义的变量没有任何关联的值,但 var 定义的变量被设置为 undefined

这是因为,在创建阶段,代码会扫描变量和函数声明,而函数声明则完整地存储在环境中,变量最初设置为 undefined (在情况下 var )或保持未初始化(在情况下)letconst )。

这就是为什么你可以 var 在声明之前访问已定义的变量(尽管 undefined )但在声明之前访问let和const变量时会出现引用错误。

这就是我们所说的提升。

# 4.2 执行阶段

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

在执行阶段,如果 JavaScript 引擎 let 在源代码中声明的实际位置找不到变量的值,那么它将分配给它的值 undefined

# 5、关于变量对象与活动对象

变量对象与活动对象的概念是 ES3 提出的老概念,从 ES5 开始就用词法环境和变量环境替代

在上文中,我们通过介绍词法环境与变量环境解释了为什么var会存在变量提升,为什么let const没有,而通过变量对象与活动对象是很难解释的,由其是在JavaScript在更新中不断在弥补当初设计的缺陷。

其次,词法环境的概念与变量对象这类概念也是可以对应上的。

我们知道变量对象与活动对象其实都是变量对象,变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。而在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。

那这不正好对应到了全局词法记录与函数词法记录了吗。而且由于ES6新增的let const不存在变量提升,于是正好有了词法环境与变量环境的概念来解释这个问题。

所以说到这,你也不用为词法环境,变量对象的概念闹冲突了。

# 6、总结

  1. 全局执行上下文一般由浏览器创建,代码执行时就会创建;函数执行上下文只有函数被调用时才会创建,调用多少次函数就会创建多少上下文。

  2. JavaScript 中有三种执行上下文类型:

    • 全局执行上下文(Global)
    • 函数执行上下文(Function)
    • eval 执行上下文(Eval)
  3. 调用栈:用于存放所有执行上下文,满足 FILO 规则(后进先出,先进后出)

  4. 执行上下文创建有两个阶段:创建阶段执行阶段

    • 创建阶段:
      • this 值的决定
        • 在全局执行上下文中,this 指向 window 对象
        • 在函数执行上下文中,this 的值取决于该函数是如何被调用的。
      • 创建词法环境组件
        • 环境记录器:存储变量和函数声明的实际位置
        • 外部环境的引用:用于访问其父级词法环境(作用域)
        • 存储 const let 声明的变量
        • 也分为两种类型:全局词法环境 和 函数词法环境
        • 环境记录器也有两种类型:
          • 在全局环境中,环境记录器是对象环境记录器
          • 在函数环境中,环境记录器是声明式环境记录器
      • 创建变量环境组件
        • 存储 var 声明的变量
    • 执行阶段:完成对所有这些变量的分配,最后执行代码

# 7、参考

[译] 理解 JavaScript 中的执行上下文和执行栈 (opens new window)

一篇文章看懂JS执行上下文 (opens new window)

JavaScript深入之变量对象 (opens new window)

JavaScript深入之执行上下文栈 (opens new window)

理解JavaScript 中的执行上下文和执行栈 (opens new window)

JS中的执行上下文(Execution Context)和栈(stack) (opens new window)

上次更新: 4/10/2022, 10:26:50 AM