深入理解 JavaScript 的执行上下文和执行栈

1,043 阅读6分钟

我正在参加「掘金·启航计划」

大家好,我是晚天

JavaScript 的执行上下文和执行栈是非常重要的概念,对于理解 JavaScript 是如何运行的至关重要。下面,我们来深入理解下这两个概念。

什么是执行上下文

执行上下文(Execution Context)是 JavaScript 执行的环境,任何 JavaScript 代码都是在执行上下文中运行的。

执行上下文类型

有三种类型的执行上下文:

  • 全局执行上下文。不再任何函数中的代码都是在全局执行上下文中执行,一个应用程序中只有一个全局执行上下文。
  • 函数执行上下文。每次函数调用,都会有一个新的该函数专有的执行上下文被创建。
  • Eval 上下文。eval 函数也拥有自己的执行上下文。

执行栈

执行栈(Execution Stack),也被称为调用栈(Calling Stack),是一个后进先出(LIFO, Last in,First out)的栈,用于存储代码执行过程中的所有执行上下文。

JavaScript 引擎在刚开始执行 JavaScript 代码的时候,会首先创建一个全局执行上下文,并将该全局执行上下文推到执行栈中。当 JavaScript 引擎遇到函数执行时,会创建一个新的函数执行上下文,并将该函数执行上下文推到执行栈顶部。

JavaScript 引擎会优先执行位于执行栈顶部的执行上下文所对应的函数,当函数执行完成后,会将该函数的执行上下文从执行栈中弹出。然后,循环执行上述过程,直至执行栈为空。

以以下代码为例,来理解执行栈的运行过程:

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

执行栈运行过程如图所示:

执行上下文如何被创建?

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

  1. 创建阶段
  2. 执行阶段

创建阶段

执行上下文在创建阶段被创建,分为以下两个步骤:

  1. 创建词法环境(Lexical Environment)
  2. 创建变量环境(Variable Environment)

因此,执行上下文可以抽象理解为:

ExecutionContext = {
  LexicalEnvironment = <ref. to LexicalEnvironment in memory>,
  VariableEnvironment = <ref. to VariableEnvironment in  memory>,
}

词法环境

ES6 对词法环境的定义如下:

词法环境是一种规范类型,用于根据ECMAScript代码的词法嵌套结构定义特定变量和函数的标识符之间的关联关系。词汇环境由一个环境记录和一个可能为空的外部词汇环境引用组成。

简单来说,词法环境是维护标识符和变量之间的映射关系的数据结构。这里的标识符是指变量或函数的名称,变量指的是真实对象(包括函数对象、数组对象)的引用或基础类型。

以以下代码片段为例:

var a = 20;
var b = 40;
function foo() {
  console.log('bar');
}

上述代码片段的词法环境为:

lexicalEnvironment = {
  a: 20,
  b: 40,
  foo: <ref. to foo function>
}

每个词法环境由以下 3 个部分组成:

  • 环境记录(Environment Record);
  • 对外部环境的引用;
  • This 绑定关系。

环境记录

环境记录是词法环境中存储变量或函数声明的地方。

有两种类型的环境记录:

  • 声明式环境记录(Declarative environment record) 。顾名思义,存储变量和函数声明。函数代码的词法环境包含一个声明性环境记录。
  • 对象环境记录(Object environment record ) 。全局代码的词汇环境包含一个对象环境记录。除了变量和函数声明,对象环境记录还存储了一个全局绑定对象(浏览器中的 Window 对象)。因此,对于每个绑定对象的属性(在浏览器中,它包含浏览器提供给 Window 对象的属性和方法)。

注意:对于函数代码,环境记录也包含 arguments 对象,该对象包含传递给函数的索引(indexs)和参数(arguments)之间的映射,以及传递给函数的参数的长度。

举例说明,以下函数的 arguments 对象如下:

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

对外部环境的引用

对外部环境的引用意味着当前词法环境拥有它外部词法环境的访问权限。当前词法环境找不到变量时,会在外部词法环境中查找。

This 绑定关系

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

在函数执行上下文中,this 的值取决于函数被调用的方式;如果被对象引用调用,则 this 指向该对象;其他情况,this 的值指向全局对象或 undefined(严格模式下)。

// 对象可以作为 bind 或 apply 的第一个参数传递,并且该参数将绑定到该对象。
var obj = {a: 'Custom'};

// 声明一个变量,并将该变量作为全局对象 window 的属性。
var a = 'Global';

function whatsThis() {
  return this.a;  // this 的值取决于函数被调用的方式
}

whatsThis();          // 'Global' 因为在这个函数中 this 没有被设定,所以它默认为 全局/ window 对象
whatsThis.call(obj);  // 'Custom' 因为函数中的 this 被设置为 obj
whatsThis.apply(obj); // 'Custom' 因为函数中的 this 被设置为 obj

词法环境可以用以下伪代码表示:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
    }
    outer: <null>,
    this: <global object>
  }
}
FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
    }
    outer: <Global or outer function environment reference>,
    this: <depends on how function is called>
  }
}

变量环境

变量环境也是一个词法环境,所以拥有前文中介绍的所有属性和特性。变量环境的环境记录用于维护变量语句创建的绑定关系。

在 ES6 中,词法环境和变量环境的区别在于,前者用于存储函数声明和变量(let 和 const)绑定关系,后者只用于存储变量绑定关系。

执行阶段

赋值和代码执行会在执行阶段完成。

举个栗子

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

当上述代码执行时,会首先在创建阶段,创建一个全局执行上下文。

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      a: < uninitialized >,
      b: < uninitialized >,
      multiply: < func >
    }
    outer: <null>,
    ThisBinding: <Global Object>
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      c: undefined,
    }
    outer: <null>,
    ThisBinding: <Global Object>
  }
}

然后,在执行阶段进行赋值。

GlobalExectionContext = {
  LexicalEnvironment: {
      EnvironmentRecord: {
        Type: "Object",
        // Identifier bindings go here
        a: 20,
        b: 30,
        multiply: < func >
      }
      outer: <null>,
      ThisBinding: <Global Object>
  },
  VariableEnvironment: {
      EnvironmentRecord: {
        Type: "Object",
        // Identifier bindings go here
        c: undefined,
      }
      outer: <null>,
      ThisBinding: <Global Object>
  }
}

当函数 multiply(20, 30) 被调用时,一个新的函数上下文被创建用于执行该函数代码。函数上下文如下:

FunctionExectionContext = {
  LexicalEnvironment: {
      EnvironmentRecord: {
        Type: "Declarative",
        // Identifier bindings go here
        Arguments: {0: 20, 1: 30, length: 2},
      },
      outer: <GlobalLexicalEnvironment>,
      ThisBinding: <Global Object or undefined>,
  },
  VariableEnvironment: {
      EnvironmentRecord: {
        Type: "Declarative",
        // Identifier bindings go here
        g: undefined
      },
      outer: <GlobalLexicalEnvironment>,
      ThisBinding: <Global Object or undefined>
  }
}

进入执行阶段,函数内赋值完成。

FunctionExectionContext = {
  LexicalEnvironment: {
      EnvironmentRecord: {
        Type: "Declarative",
        // Identifier bindings go here
        Arguments: {0: 20, 1: 30, length: 2},
      },
      outer: <GlobalLexicalEnvironment>,
      ThisBinding: <Global Object or undefined>,
  },
  VariableEnvironment: {
      EnvironmentRecord: {
        Type: "Declarative",
        // Identifier bindings go here
        g: 20
      },
      outer: <GlobalLexicalEnvironment>,
      ThisBinding: <Global Object or undefined>
  }
}

函数执行完成后,函数返回值赋值给变量 c,全局词法环境被更新。然后,全局代码执行完成,程序结束。

参考资料