# 执行上下文与执行上下文栈

# 目录

# 执行上下文

执行上下文是评估和执行 JavaScript 代码的环境的抽象概念,JavaScript 中运行的任何代码都是在执行上下文中运行的。

# 执行上下文类型

1️⃣ 全局执行上下文

这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:

  • 创建一个全局的 window 对象(浏览器的情况下),
  • 并且设置 this 的值等于这个全局对象。

一个程序中只会有一个全局执行上下文。

2️⃣ 函数执行上下文

每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。

3️⃣ eval 函数执行上下文

eval 函数中执行的代码也会有自己的自行上下文,但由于 eval 已经不常用了,所以不做讨论。

# 执行栈

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

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

上述代码的执行栈

当上述代码加载到浏览器中时,Javascript 引擎将创建一个全局执行上下文并将其推送到当前执行栈。当遇到对 first() 的调用时,JavaScript 引擎会为该函数创建一个新的执行上下文,并将其推入当前执行栈的顶部。

当从 first() 函数内部调用 second() 函数时,JavaScript 引擎会为该函数创建一个新的执行上下文,并将其推入当前执行栈的顶部。当 second() 函数完成时,其的执行上下文将从当前栈中弹出,并且控制到达其下面的执行上下文,即 first() 函数执行上下文。

first()完成时,将从栈中删除其执行栈,并将控制权移至全局执行上下文。一旦执行完所有代码,JavaScript 引擎就会从当前栈中删除全局执行上下文。

# 如何创建执行上下文?

执行上下文分为两个阶段:1)创建阶段和 2)执行阶段

# 1️⃣ 创建阶段

执行上下文是在「创建阶段」被创建的,「创建阶段」包括以下几个方面:

  1. 创建词法环境
  2. 创建变量环境

因此,「执行上下文」可以在概念上表示为以下形式:

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

⭐️ 词法环境

ES6官方文档 (opens new window)将词法环境定义为:

词法环境(Lexical Environments)是一种规范类型,用于根据ECMAScript代码的词法嵌套结构来定义标识符与特定变量和函数的关联。词法环境由一个「环境记录」(Environment Record)和一个可能为空的「外部词法环境」(outer Lexical Environment)引用组成。

简单来说,词法环境就是一种标识符—变量映射的结构(这里的标识符指的是变量/函数的名字,变量是对实际对象[包含函数和数组类型的对象]或基础数据类型的引用)。

例如,考虑以下代码片段:

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

因此,以上代码段的词法环境如下所示:

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

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

  1. 环境记录
  2. 外部环境引用
  3. 绑定 this

⭐️ 1. 环境记录

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

环境记录也有两种类型:

  1. 声明类环境记录。顾名思义,它存储的是变量和函数声明,「函数的词法环境内部就包含着一个声明类环境记录」 。
  2. 对象环境记录。「全局环境中的词法环境中包含的就是一个对象环境记录」。除了变量和函数声明外,对象环境记录还包括全局对象(浏览器的 window 对象)。因此,对于对象的每一个新增属性(对浏览器来说,它包含浏览器提供给 window 对象的所有属性和方法),都会在该记录中创建一个新条目。

注意:对函数而言,环境记录还包含一个 arguments 对象,该对象是个类数组对象,包含参数索引和参数的映射以及一个传入函数的参数的长度属性。

⭐️ 说明:环境记录对象在「创建阶段」也被称为「变量对象(VO)」,在「执行阶段」被称为「活动对象(AO)」。之所以被称为变量对象是因为此时该对象只是存储执行上下文中变量和函数声明,之后代码开始执行,变量会逐渐被初始化或是修改,然后这个对象就被称为活动对象。

⭐️ 2. 外部环境引用

对「外部环境的引用」意味着它可以访问其外部词法环境。这意味着,如果在当前词法环境中找不到变量,那么JavaScript 引擎可以在外部环境中查找变量。

⭐️ 3. 绑定 this

在词法环境创建阶段中,会确定 this 的值。

在全局执行上下文中,this 值引用 global 对象。 (在浏览器中,这是指 window 对象)。

在函数执行上下文中,此值取决于函数的调用方式。如果由对象引用调用它,则将 this 值设置为该对象,否则,将 this 值设置为全局对象或 undefined(在严格模式下)。例如:

const person = {
  name: 'peter',
  birthYear: 1994,
  calcAge: function() {
    console.log(2018 - this.birthYear);
  }
}
person.calcAge(); 
// 'this' 指的是'person',因'calcAge' 是用 'person' 对象引用调用的
const calculateAge = person.calcAge;
calculateAge();
// 'this' 指的是 window 对象,因为没有给出对象引用

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

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>
  }
}

⭐️ 词法环境类型

  1. 全局词法环境(在全局上下文中):

    是一个没有外部环境的词法环境,全局环境的外部环境引用为 null,它拥有一个全局对象(浏览器的 window 对象)及其关联的方法和属性(例如数组方法)以及任何用户自定义的全局变量,this 的值指向这个全局对象。

  2. 函数词法环境:

    在函数中定义的变量被存储在环境记录中,对外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境;对函数而言,环境记录还包含一个 arguments 对象,该对象是个类数组对象,包含参数索引和参数的映射以及一个传入函数的参数的长度属性。

⭐️ 变量环境

其实「变量环境」也是词法环境的一种,它的环境记录包含了变量声明语句在执行上下文中创建的变量和具体值的绑定关系。

如上所述,变量环境也是词法环境的一种,因此它具有词法环境所有的属性。

在ES6中,词法环境和变量环境的不同就是前者用来存储函数声明和变量声明(letconst绑定关系,后者只用来存储var 声明的变量绑定关系。

# 2️⃣ 执行阶段

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

  1. 赋值
  2. 词法环境用于解析绑定
    • 词法环境:最初,它只是环境变量的一个副本,在运行的上下文中,它用于确定出现在上下文中的标识符的绑定
    • 在执行阶段之后,借助词法环境,变量环境表被赋值(填充)
      • 每个执行上下文都有用于标识符解析的词法环境。上下文的所有本地绑定都存储于环境记录表中。如果在当前环境记录中没有解析标识符,解析过程将继续道外部(父)环境记录表。此模式将继续,直到标识符被解析位置。如果没有找到,抛出 ReferenceError。
      • 这与原型查找链非常相似。现在这里要记住的关键是词法环境在上下文创建(静态地)从词汇上捕获外部绑定,并在运行上下文(执行阶段)中使用。即词法环境是根据代码的位置来决定的,词法作用域是代码阶段就决定好的,和函数是怎么调用的没有关系。
      • 在创建阶段的所有函数都静态地(从词汇上)捕获其父环境的外部绑定。这允许嵌套函数访问外部绑定,即使父上下文已从执行堆栈中清除。这种机制是 JavaScript 中闭包的基础。

# 例子

让我们看一些例子来理解以上概念:

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",
      // 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>
  }
}

当遇到对函数 multiple(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 中。因此,全局词法环境已更新。之后,全局代码完成,程序结束。

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

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

这就是为什么您可以在声明 var 定义变量之前访问它们(尽管 undefined 未定义),但是在声明 letconst 变量之前访问它们时却获得引用错误的原因。

这就是我们所谓的提升。

注意:在「执行阶段」,如果 JavaScript 引擎找不到 letconst 声明的变量的值,也会被赋值为 undefined

# 参考: