Contents

JavaScript 执行上下文与执行栈

JavaScript 执行上下文与执行栈

执行上下文是当前 JS 代码被解析和执行时所在环境的抽象概念, 而理解执行上下文和执行栈对于理解其他 JS 概念 (如变量声明提升, 作用域和闭包) 至关重要.

执行上下文的类型

JS 中有三种执行上下文类型:

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

执行栈

执行栈, 也就是在其它编程语言中所说的 调用栈 , 是一种拥有 LIFO (后进先出) 数据结构的栈, 被用来存储代码运行时创建的所有执行上下文.

首次运行 JS 代码时, 会创建一个全局执行上下文并 Push 到当前的执行栈中. 每当发生函数调用, 引擎都会为该函数创建一个新的函数执行上下文并 Push 到当前执行栈的栈顶.

根据执行栈 LIFO 规则, 当栈顶函数运行完成后, 其对应的函数执行上下文将会从执行栈中 Pop 出, 上下文控制权将移到当前执行栈的下一个执行上下文.

const a = 'Hello World!';

function first() {
  console.log('执行 first 函数');
  second();
  console.log('继续执行 first 函数');
}

function second() {
  console.log('执行 second 函数');
}

first();
console.log('继续执行属于全局上下文的log');

// 执行 first 函数
// 执行 second 函数
// 继续执行 first 函数
// 继续执行属于全局上下文的log

当上述代码在浏览器加载时, JS 引擎创建了一个全局执行上下文并把它压入当前执行栈. 当遇到 first() 函数调用时, JS 引擎为该函数创建一个新的执行上下文并把它压入当前执行栈的顶部.

当从 first() 函数内部调用 second() 函数时, JS 引擎为 second() 函数创建了一个新的执行上下文并把它压入当前执行栈的顶部. 当 second() 函数执行完毕, 它的执行上下文会从当前栈弹出, 并且控制流程到达下一个执行上下文, 即 first() 函数的执行上下文.

first() 执行完毕, 它的执行上下文从栈弹出, 控制流程到达全局执行上下文. 一旦所有代码执行完毕, JS 引擎从当前栈中移除全局执行上下文.

执行上下文的创建

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

创建阶段

在 JS 代码执行前, 执行上下文将经历创建阶段, 而在创建阶段会发生三件事:

  1. 绑定 this 指针
  2. 创建 词法环境 组件
  3. 创建 变量环境 组件

其中执行上下文的概念表示如下:

interface ExecutionContext {
  // 绑定 `this` 指针
  ThisBinding: this;
  // 创建 `词法环境` 组件
  LexicalEnvironment: any;
  // 创建 `变量环境` 组件
  VariableEnvironment: any;
}

This 的绑定

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

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

function f1() {
  return this;
}
//在浏览器中:
f1() === window; //在浏览器中, 全局对象是 window

//在 Node 中:
f1() === globalThis;

// 在严格模式下 this 会保持为 undefined
function f2() {
  'use strict'; // 这里是严格模式
  return this;
}

f2() === undefined; // true

词法环境 (Lexical Environment)

参考: 词法环境 (Lexical Environment

变量环境

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

如上所述, 变量环境也是一个词法环境, 所以它有着上面定义的词法环境的所有属性.

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

我们看点样例代码来理解上面的概念:

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

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

c = multiply(20, 30);

执行上下文看起来像这样:

interface GlobalExecutionContext {
  ThisBinding: Global;

  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: 'Object';
      // Uninitialized 不同等于 undefined
      a: Uninitialized;
      b: Uninitialized;
      multiply: Function;
    };
    outer: null;
  };

  VariableEnvironment: {
    EnvironmentRecord: {
      Type: 'Object';
      c: undefined;
    };
    outer: null;
  };
}

interface FunctionExecutionContext {
  ThisBinding: Global;

  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: 'Declarative';
      Arguments: { 0: 20; 1: 30; length: 2 };
    };
    outer: GlobalLexicalEnvironment;
  };

  VariableEnvironment: {
    EnvironmentRecord: {
      Type: 'Declarative';
      g: undefined;
    };
    outer: GlobalLexicalEnvironment;
  };
}

注意 — 只有遇到调用函数 multiply 时, 函数执行上下文才会被创建.

可能你已经注意到 letconst 定义的变量并没有关联任何值, 但 var 定义的变量被设成了 undefined

这是因为在创建阶段时, 引擎检查代码找出变量和函数声明, 虽然函数声明完全存储在环境中, 但是变量最初设置为 undefined (var 情况下), 或者未初始化 (letconst 情况下).

这就是为什么你可以在声明之前访问 var 定义的变量 (虽然是 undefined), 但是在声明之前访问 letconst 的变量会得到一个引用错误. 这就是我们说的变量声明提升.

执行阶段

这是整篇文章中最简单的部分. 在此阶段, 完成对所有这些变量的分配, 最后执行代码.

注意 — 在执行阶段, 如果 JS 引擎不能在源码中声明的实际位置找到 let 变量的值, 它会被赋值为 undefined.

结论

我们已经讨论过 JS 程序内部是如何执行的. 虽然要成为一名卓越的 JS 开发者并不需要学会全部这些概念, 但是如果对上面概念能有不错的理解将有助于你更轻松, 更深入地理解其他概念, 如变量声明提升, 作用域和闭包.

参考链接

【进阶 1-1 期】理解 JavaScript 中的执行上下文和执行栈#12 [译] 理解 JavaScript 中的执行上下文和执行栈