再谈 JS 执行上下文(ES6版)

39 阅读7分钟

前言

作为和 JS 同步进化的底层基础概念,执行上下文的构成有很多版本,在我的上篇博客《深入理解JS(三) - 执行上下文、作用域链与闭包》中,为了方便理解作用域链和闭包,我采用了 ES3 的版本的执行上下文。
现在讲究与时俱进,我将在这篇博客中为大家补充理解 ES6 版本的执行上下文。

一. 定义

不管版本如何变化,执行上下文就是代码的执行环境,其中包含了这段代码执行所需的所有信息。

二. 构成

与 ES3 中的执行上下文构成(变量对象、作用域链、this 指针)不同,ES6 中的执行上下文由词法环境、变量环境、this 绑定三部分构成。其中词法环境、变量环境 = ES3的变量对象 + 作用域链。

2. 执行上下文示意图2.png

  • 生命周期: 执行上下文的生命周期按照 创建阶段 -> 执行阶段 -> 销毁阶段 的顺序依次进行。

  • 在执行上下文的创建阶段,按照 this 绑定 -> 词法环境组件创建 -> 变量环境组件创建 的顺序进行创建。

    1. 执行上下文示意图1.png

词法环境(LexicalEnvironment)

因为 ES6 新增了块级作用域以及支持块级作用域的声明方式letconst,所以执行上下文也随之作出了改进,引入了词法环境,用于支持块级作用域。关于提升和TDZ的问题,本博客不做探讨,请移步《深入理解JS(一) - 提升与TDZ》

  • 其中存储letconstfunctionclassimport声明的变量。
  • 示例:
    • letconst声明:

      function example() {
          // 词法环境(LexicalEnvironment)
          let a = 10;        // let声明的变量
          const b = 20;      // const声明的常量
      
          if (true) {
              let c = 30;      // 块级作用域内的let声明
              const d = 40;    // 块级作用域内的const声明
          }
      
          // 变量环境(VariableEnvironment)
          var e = 50;        // var声明的变量(归变量环境)
      }
      
    • function声明:

      function outer() {
          // 词法环境(LexicalEnvironment)
          function inner() {      // function声明(属于词法环境)
              console.log('这是一个函数声明');
          }
      
          // 变量环境(VariableEnvironment)
          var temp = "temporary";
      
          // 函数声明会被提升到词法环境顶部,可以在声明前调用
          inner(); // 正常执行:"这是一个函数声明"
      }
      
      outer();
      
    • class声明:

      function testClass() {
      // 词法环境(LexicalEnvironment)
      class Person {       // class声明
          constructor(name) {
              this.name = name;
          }
      }
      
      const p = new Person("张三");
      
      // 变量环境(VariableEnvironment)
      var temp = "temporary";
      }
      
    • import声明:

      // module.js
      export const PI = 3.14;
      export function sum(a, b) {
          return a + b;
      }
      
      // main.js(ES模块环境)
      import { PI, sum } from './module.js';  // import声明
      
      function calculate() {
          // 词法环境(LexicalEnvironment)
          console.log(PI);       // 导入的常量
          const result = sum(1, 2);  // 导入的函数
      
          // 变量环境(VariableEnvironment)
          var message = "Result: " + result;
      }
      

变量环境(VariableEnvironment)

变量环境是词法环境的一种特殊类型,JS 引入letconst后,因为var声明不支持块级作用域,专门分离出来用于存储var声明。

  • 其中只存储var声明的变量。
  • 示例:
    • var声明的变量:

      function compareEnvironments() {
          var x = 1; // 存储在变量环境中
          let y = 2; // 存储在词法环境中
      
          if (true) {
          var x = 10; // 覆盖函数级变量环境中的 x (var声明不支持块级作用域)
          let y = 20; // 创建块级词法环境中的新变量 y
          console.log(x, y); // 输出:10 20
          }
      
          console.log(x, y); // 输出:10 2(词法环境中的 y 未受影响)
      }
      
      compareEnvironments();
      

this 绑定(This Binding)

这里的 this 绑定大致与 ES3 版本的 this 指针指向规则一致,即为由调用方式决定,但是有个别不同。

  • 普通函数与箭头函数: ES6 出现了箭头函数,使得 this 的指向出现了变化。
    • 普通函数: this的值取决于函数的调用方式(全局、函数、构造函数、方法调用等),动态绑定。
    • 箭头函数: this继承自外层执行上下文,不随调用方式改变,静态绑定。
  • 全局作用域与模块作用域: 在 ES6 新引入的模块作用域情况下,this 的指向也有不同。
    • 全局作用域: 全局作用域中this指向全局对象(如浏览器中的window以及Node.js中的global)。
    • 模块作用域: 全局作用域中this仍指向全局对象,但模块作用域中thisundefined

三. 词法环境(Lexical Environment)

在上面的描述中,我们称ES6的执行上下文构成为词法环境、变量环境、this 绑定,但是其实应该称呼它们为词法环境组件、变量环境组件、this 绑定三部分。这是因为词法环境组件变量环境组件看似割裂,其实同为一族,均为 词法环境(Lexical Environment) 中的一员。

  • 将它们区别开来是为了实现块级作用域的同时不影响var变量声明和函数声明。词法环境组件存储 需要块级作用域的声明let/const/class/import)和 函数声明(无论是否在块内);而变量环境组件存储var声明的变量。

  • 词法环境组件与变量环境组件的关系: 前面我们说过变量环境是词法环境的一种特殊类型,如果要打个比方,那就是 长方形(词法环境组件)与正方形(变量环境组件) 的关系。

    3. 执行上下文示意图3.png

构成

对于同属词法环境的词法环境组件和变量环境组件来说,它们二者都由 环境记录(Environment Record)、外部引用(outer) 构成,拥有相同的基础结构。

  • 详细结构示例:

    4. 词法环境示意图1.png

  • 环境记录: 环境记录存储变量和函数的定义,并管理作用域内的标识符绑定。ES6 将其分为声明式环境记录和对象式环境记录。

    • 声明式环境记录:存储通过 letconstvarfunctionclassimport 显式声明的标识符。其又可以细分为函数环境记录和模块环境记录。
      • 函数环境记录(Function Environment Record): 每个函数调用生成独立记录,包含参数绑定、this 值、arguments 对象(非箭头函数)及词法作用域引用。
      • 模块环境记录(Module Environment Record): 每个 ES6 模块独有,管理 import/export 绑定、模块顶层作用域(var 不挂载全局),确保模块单例执行。
    • 对象式环境记录: 将标识符绑定到某个对象的属性上,实现动态作用域。常用于全局环境(比如window对象或者global对象)以及with语句环境。
  • 外部引用(outer): 指向外部词法环境的引用,形成作用域链。当查找变量时,JavaScript 引擎会先在当前环境记录中查找,若未找到则沿 outer 引用逐级向上查找,直到全局环境。(其实就是ES3的作用域链)

共享的外部引用(outer)

5. 执行上下文示意图4.png 看到上图,你应该明白了,同一执行上下文的词法环境组件和变量环境组件,它们所使用的外部引用其实是同一个。二者的 外部引用(outer) 指向相同的父级词法环境,共同构成完整的作用域链。

  • 作用:
    • 当查找变量时,无论从词法环境组件开始,还是从变量环境组件开始,最终都会沿相同的路径向上查找。
    • 闭包可以同时访问词法环境组件和变量环境组件中的变量,保证了变量查找的一致性。
  • 变量查找机制:
    1. 现在当前执行上下文的词法环境组件(优先级最高)中查找;
    2. 无果后,在当前执行上下文的变量环境组件中查找;
    3. 无果后,沿着外部引用(outer)继续向上逐级查找。
  • 顺序: 词法环境 -> 变量环境 -> outer

四. 结语

JS 的执行上下文真的是越往后越复杂,各种声明的归属因为版本不同错综复杂,很多东西我不确定,所以没有写出来,等我以后搞懂了,再细写一篇理一理这些版本。
希望这篇博客能对您有所帮助,如果有错误,请您指出,不胜感激。