JavaScript 执行上下文(Execution Context)全过程:从变量提升到词法环境创建

各位 JavaScript 开发者,欢迎来到今天的讲座。我们即将深入探索 JavaScript 引擎的核心机制之一:执行上下文(Execution Context)。这个概念是理解变量提升(Hoisting)、作用域(Scope)、闭包(Closures)以及 this 关键字行为的关键。掌握执行上下文,意味着你能够更准确地预测代码行为,编写更健壮、更高效的 JavaScript。

JavaScript 代码在执行时,并不是简单地从上到下逐行运行。引擎会创建一个特定的环境来处理当前的代码块,这个环境就是执行上下文。想象一下,每当 JavaScript 引擎准备执行一段代码时,它都会为其搭建一个“舞台”。这个舞台就是执行上下文,它包含了代码运行所需的所有信息。

第一章:执行上下文——代码执行的舞台

1.1 什么是执行上下文?

执行上下文是 JavaScript 引擎在执行代码时创建的一种抽象概念环境。它是一个包含了当前代码执行所需的变量、函数、作用域链以及 this 值的完整环境。每当 JavaScript 代码执行时,它总是在某个执行上下文内部运行。

1.2 为什么执行上下文如此重要?

  • 变量管理: 它决定了哪些变量和函数在当前代码块中是可访问的,以及它们是如何被查找的。
  • 作用域: 它定义了变量和函数的可访问范围,这是理解闭包的基础。
  • this 关键字: 它决定了 this 关键字在函数中的指向。
  • 执行流程: 它构成了 JavaScript 代码的调用栈,管理着函数的调用顺序。

1.3 执行上下文的类型

JavaScript 中主要有三种类型的执行上下文:

  1. 全局执行上下文 (Global Execution Context – GEC):
    这是最基础的执行上下文。当 JavaScript 文件首次加载到浏览器或 Node.js 环境中时,就会创建一个全局执行上下文。它在整个应用程序的生命周期中只存在一个。

    • 在浏览器中,全局对象是 window
    • 在 Node.js 中,全局对象是 global
    • this 关键字在全局执行上下文的顶层代码中指向全局对象。
  2. 函数执行上下文 (Function Execution Context – FEC):
    每当调用一个函数时,都会创建一个新的函数执行上下文。每个函数都有自己的执行上下文,即使是同一个函数被调用多次,每次调用都会创建一个独立的执行上下文。这是 JavaScript 实现函数局部作用域的关键。

  3. eval 函数执行上下文 (Eval Execution Context):
    当使用 eval() 函数执行代码时,会创建一个 eval 执行上下文。然而,由于其安全性和性能问题,通常不建议使用 eval,因此在实际开发中较少遇到。我们将主要关注全局和函数执行上下文。

1.4 执行上下文栈 (Call Stack)

JavaScript 引擎使用一个堆栈(LIFO – 后进先出)来管理所有的执行上下文,这个堆栈被称为“执行上下文栈”或“调用栈”。

  • 当 JavaScript 代码开始执行时,全局执行上下文首先被推入栈底。
  • 每当一个函数被调用时,一个新的函数执行上下文就会被创建并推入栈顶。
  • 当函数执行完毕后,其对应的执行上下文会从栈中弹出,控制权返回到栈中下一个(下方)的执行上下文。
  • 当所有代码执行完毕,全局执行上下文也会从栈中弹出,栈变空。

第二章:执行上下文的解剖

每个执行上下文在创建时,都会包含几个重要的组成部分。虽然 JavaScript 引擎的实现细节可能有所不同,但从概念上讲,我们可以将其主要组件归纳为:

  1. 词法环境 (Lexical Environment):存储变量和函数声明的实际位置,并维护对其外部环境的引用,构成作用域链。
  2. 变量环境 (Variable Environment):在 ES6 之前,它与词法环境概念上相同。在 ES6 之后,它主要负责处理 var 和函数声明的绑定。可以理解为词法环境的一个子集或初始状态,专门用于处理历史遗留的 var 变量提升行为。
  3. this 绑定 (This Binding):确定当前执行上下文中的 this 关键字指向哪个对象。

现在,让我们深入探讨这些组件。

第三章:第一阶段:创建阶段——蓝图的绘制

当 JavaScript 引擎调用一个函数(或加载全局代码)时,它不会立即执行代码。首先,它会进入“创建阶段”,在这个阶段为即将运行的代码搭建好舞台——即初始化执行上下文的各个组件。

3.1 确定 this 绑定

this 关键字是 JavaScript 中一个臭名昭著的难点,它的值在函数被调用时才确定,取决于函数的调用方式。在执行上下文的创建阶段,this 的值就会被绑定。

3.1.1 全局执行上下文中的 this

在全局执行上下文的顶层代码中,this 总是指向全局对象。

  • 在浏览器中是 window 对象。
  • 在 Node.js 中是 global 对象。
// 在浏览器环境中
console.log(this === window); // true

// 在 Node.js 环境中
console.log(this === global); // true (注意:在模块顶部,this可能指向模块导出对象,但在REPL或直接脚本中通常是global)

var globalVar = "I am global";
console.log(window.globalVar); // "I am global" (浏览器)
console.log(global.globalVar); // "I am global" (Node.js)

3.1.2 函数执行上下文中的 this

函数中的 this 绑定规则更为复杂,主要取决于函数的调用方式:

  1. 默认绑定 (Default Binding)
    当函数作为普通函数调用,且不属于任何对象时,this 默认指向全局对象(在严格模式下是 undefined)。

    function showThis() {
      console.log(this);
    }
    
    showThis(); // 浏览器: window; Node.js: global
    
    function showThisStrict() {
      "use strict";
      console.log(this);
    }
    
    showThisStrict(); // undefined (严格模式下)
  2. 隐式绑定 (Implicit Binding)
    当函数作为对象的方法被调用时,this 指向调用该方法的对象。

    var person = {
      name: "Alice",
      greet: function () {
        console.log("Hello, my name is " + this.name);
      },
      nested: {
        name: "Bob",
        greet: function () {
          console.log("Hello, my name is " + this.name);
        },
      },
    };
    
    person.greet(); // Hello, my name is Alice (this -> person)
    person.nested.greet(); // Hello, my name is Bob (this -> person.nested)
    
    var greetFunc = person.greet;
    greetFunc(); // Hello, my name is undefined (this -> window/global, 丢失了隐式绑定)
  3. 显式绑定 (Explicit Binding)
    使用 call(), apply(), bind() 方法可以强制改变 this 的指向。

    function sayName(greeting) {
      console.log(greeting + ", my name is " + this.name);
    }
    
    var anotherPerson = {
      name: "Charlie",
    };
    
    sayName.call(anotherPerson, "Hi"); // Hi, my name is Charlie (this -> anotherPerson)
    sayName.apply(anotherPerson, ["Hello"]); // Hello, my name is Charlie (this -> anotherPerson)
    
    var boundSayName = sayName.bind(anotherPerson, "Greetings");
    boundSayName(); // Greetings, my name is Charlie (bind返回一个新函数,其this永久绑定)
  4. new 绑定 (New Binding)
    当函数作为构造函数与 new 关键字一起使用时,this 指向新创建的对象实例。

    function Car(make, model) {
      this.make = make;
      this.model = model;
      console.log(this); // 指向新创建的Car实例
    }
    
    var myCar = new Car("Honda", "Civic"); // myCar 是一个新对象,this指向myCar
  5. 箭头函数 (Arrow Functions)
    箭头函数没有自己的 this 绑定。它会捕获其定义时所处的“外部(lexical)”执行上下文的 this 值。这意味着箭头函数的 this 不会随着调用方式的改变而改变。

    var counter = {
      count: 0,
      start: function () {
        // 传统函数,this指向counter
        console.log("Traditional function this:", this); // { count: 0, start: [Function: start], increment: [Function: increment] }
        setTimeout(function () {
          console.log("Traditional callback this:", this); // window/global
        }, 100);
      },
      increment: function () {
        // 箭头函数,this在定义时捕获了外部的this (即start方法中的this,指向counter)
        console.log("Arrow function this:", this); // { count: 0, start: [Function: start], increment: [Function: increment] }
        setTimeout(() => {
          console.log("Arrow callback this:", this); // { count: 0, start: [Function: start], increment: [Function: increment] }
          this.count++;
          console.log("Count:", this.count);
        }, 100);
      },
    };
    
    counter.start();
    counter.increment();

3.2 设置词法环境 (Lexical Environment – LE)

词法环境是执行上下文最重要的组成部分之一。它是一个内部数据结构,用于存储当前执行上下文中声明的所有变量和函数的标识符到其值的映射。每个词法环境由两部分组成:

  1. 环境记录 (Environment Record – ER)
    一个实际存储变量和函数声明的地方。
  2. 外部环境引用 (Outer Environment Reference – OER)
    一个指向其父级词法环境的引用,用于实现作用域链。

3.2.1 环境记录 (Environment Record)

环境记录是词法环境的核心,它是一个包含所有绑定(变量名到值的映射)的记录。根据代码的类型,环境记录可以是以下两种:

  • 声明式环境记录 (Declarative Environment Record)
    用于函数执行上下文和块级作用域(由 letconst 声明的变量)。它直接存储变量和函数的声明。

  • 对象环境记录 (Object Environment Record)
    主要用于全局执行上下文。它会将变量和函数声明作为属性添加到绑定对象(如 windowglobal)上。

在创建阶段,引擎会扫描当前代码,识别所有变量和函数声明,并将它们添加到环境记录中。这个过程就是我们常说的“变量提升”。

变量提升 (Hoisting) 详解

变量提升是 JavaScript 在执行上下文创建阶段的一个行为,它会将变量和函数声明“提升”到当前作用域的顶部。然而,提升的只是声明,而不是赋值。

  • 函数声明的提升: 函数声明会被完整地提升到作用域顶部,这意味着你可以在声明之前调用它们。

    sayHello(); // "Hello!"
    
    function sayHello() {
      console.log("Hello!");
    }
  • var 变量的提升: var 声明的变量会被提升,但只提升声明,并初始化为 undefined。赋值操作留在原地。

    console.log(myVar); // undefined
    var myVar = 10;
    console.log(myVar); // 10
    
    // 引擎的实际处理:
    // var myVar; // 提升并初始化为 undefined
    // console.log(myVar); // undefined
    // myVar = 10;
    // console.log(myVar); // 10
  • letconst 变量的提升与暂时性死区 (Temporal Dead Zone – TDZ):
    letconst 声明的变量也会被提升,但它们不会像 var 那样被初始化为 undefined。它们会处于一个“未初始化”状态,直到代码执行到它们实际声明的那一行。在声明行之前访问这些变量会导致 ReferenceError。这个从作用域开始到变量声明之间的区域被称为“暂时性死区”。

    console.log(myLet); // ReferenceError: Cannot access 'myLet' before initialization
    let myLet = 20;
    console.log(myLet); // 20
    
    // 引擎的实际处理:
    // myLet; // 变量被创建,但处于未初始化状态 (TDZ)
    // console.log(myLet); // 尝试访问未初始化的变量,抛出错误
    // myLet = 20; // 变量被初始化,离开TDZ
    // console.log(myLet);

变量声明在环境记录中的状态

声明类型 创建阶段(环境记录)中的行为 状态
function 函数的标识符和函数体被完整地添加到环境记录中,并且可以直接访问。 可访问
var 变量的标识符被添加到环境记录中,并初始化为 undefined 可访问,值为 undefined
let, const 变量的标识符被添加到环境记录中,但不会被初始化。它们处于“未初始化”状态,直到实际声明代码被执行。在未初始化状态下访问它们会抛出 ReferenceError,这个区域就是暂时性死区(TDZ)。 存在于环境记录中,但处于“未初始化”状态(暂时性死区)

3.2.2 外部环境引用 (Outer Environment Reference)

每个词法环境都有一个 outer 引用,指向其外部(父级)词法环境。这个引用是创建作用域链的关键。当 JavaScript 引擎需要查找一个变量时,它会首先在当前执行上下文的词法环境中查找。如果找不到,它会沿着 outer 引用向上查找,直到找到该变量或到达全局执行上下文。如果仍然找不到,就会抛出 ReferenceError

  • 全局执行上下文: 它的 outer 引用通常是 null,因为它没有外部环境。
  • 函数执行上下文: 它的 outer 引用指向函数定义时所处的那个词法环境,而不是函数调用时所处的环境。这是理解闭包的核心。
var globalVar = "Global"; // GEC

function outerFunction() {
  var outerVar = "Outer"; // FEC for outerFunction
  function innerFunction() {
    var innerVar = "Inner"; // FEC for innerFunction
    console.log(innerVar); // Found in innerFunction's LE
    console.log(outerVar); // Not in innerFunction's LE, look up to outerFunction's LE
    console.log(globalVar); // Not in innerFunction's LE, nor outerFunction's LE, look up to GEC
  }
  innerFunction();
}

outerFunction();

在这个例子中:

  • innerFunction 的词法环境的 outer 引用指向 outerFunction 的词法环境。
  • outerFunction 的词法环境的 outer 引用指向全局执行上下文的词法环境。
  • 全局执行上下文的词法环境的 outer 引用指向 null

这就是作用域链,它决定了变量的查找顺序。

3.3 设置变量环境 (Variable Environment – VE)

在 ES6 之前,词法环境和变量环境在概念上几乎是相同的。在 ES6 引入 letconst 后,为了兼容 var 的变量提升行为和块级作用域,它们被区分开来。

  • 变量环境 (VE):它是一个 Lexical Environment,其环境记录专门存储由 var 声明的变量和函数声明。在执行上下文的创建阶段,var 变量会被初始化为 undefined,函数声明会被完整地存储。
  • 词法环境 (LE):在 ES6+ 中,它现在是主要的变量存储和查找机制。它不仅包含 var 和函数声明(这些通常会从 VE 复制或引用过来),还会处理 letconst 声明。letconst 变量在 LE 的环境记录中被创建,但处于“未初始化”状态(TDZ)。

可以这样理解:

  1. 创建阶段开始
    • 首先创建 变量环境 (VE)。它的环境记录会被填充所有 var 变量(初始化为 undefined)和函数声明(初始化为实际函数)。
    • 然后创建 词法环境 (LE)。它的环境记录最初是 VE 的一个副本或包含其绑定。但更重要的是,LE 会处理 letconst 声明,将它们添加到自己的环境记录中,并标记为“未初始化”。
  2. 执行阶段
    • 引擎主要使用 词法环境 (LE) 进行变量查找。
    • 当代码执行到 letconst 的声明行时,它们在 LE 中的状态会从“未初始化”变为“已初始化”,然后被赋值。

这种区分确保了 var 的旧行为得以保留,同时 letconst 能够实现块级作用域和暂时性死区。

表格:创建阶段执行上下文的初始化

组件 全局执行上下文 函数执行上下文
this 绑定 指向全局对象 (windowglobal) 取决于函数如何调用 (默认、隐式、显式、new、箭头函数)
词法环境 (LE)
– 环境记录 (ER) 对象环境记录:所有 var 和函数声明作为全局对象的属性;let/const 作为独立绑定 声明式环境记录:存储函数参数、局部 var 变量 (初始化为 undefined)、局部函数声明、局部 let/const 变量 (初始化为“未初始化”状态,进入 TDZ)
– 外部环境引用 null 指向函数定义时所处的词法环境
变量环境 (VE)
– 环境记录 (ER) 对象环境记录:所有 var 和函数声明作为全局对象的属性 声明式环境记录:存储函数参数、局部 var 变量 (初始化为 undefined)、局部函数声明
– 外部环境引用 null 指向函数定义时所处的词法环境 (与 LE 的外部环境引用相同)

注意:在现代 JavaScript 中,通常会将 VE 视为 LE 的一个子集或在创建阶段 LE 的一个特定初始化部分,尤其是在 var 和函数声明的处理上。在实际执行中,变量查找主要发生在 LE 中。

第四章:第二阶段:执行阶段——代码的运行

在执行上下文的创建阶段完成后,所有变量和函数声明都已安置妥当,this 绑定也已确定。现在,JavaScript 引擎进入“执行阶段”,开始逐行执行代码。

4.1 变量赋值与函数执行

在这个阶段:

  • 之前被初始化为 undefinedvar 变量会被赋予它们在代码中定义的实际值。
  • 处于暂时性死区 (TDZ) 的 letconst 变量,当代码执行到它们的声明行时,会离开 TDZ 并被初始化和赋值。
  • 函数会被实际调用和执行。
var a = 10; // 执行阶段:a 从 undefined 被赋值为 10
let b = 20; // 执行阶段:b 离开 TDZ,被赋值为 20

function multiply(x, y) {
  // 创建 FEC
  let result = x * y; // 执行阶段:result 离开 TDZ,被赋值
  return result;
}

let product = multiply(a, b); // 执行阶段:调用 multiply 函数

4.2 作用域链的交互

在执行阶段,当代码尝试访问一个变量时,JavaScript 引擎会按照以下步骤进行查找:

  1. 在当前词法环境的环境记录中查找:
    引擎首先检查当前执行上下文的词法环境的环境记录。如果找到变量,就使用它。

  2. 沿着外部环境引用向上查找:
    如果当前词法环境中没有找到变量,引擎会沿着 outer 引用,向上移动到父级词法环境,并在其环境记录中查找。这个过程会一直持续。

  3. 到达全局词法环境:
    如果最终到达全局词法环境仍然没有找到变量,那么就会抛出 ReferenceError

这种查找机制就是作用域链。

第五章:深入理解词法环境与作用域

5.1 全局词法环境

  • 特点: 全局执行上下文的词法环境是作用域链的顶端。
  • 绑定对象: 在浏览器中,它的环境记录是一个对象环境记录,所有 var 声明和函数声明都会成为 window 对象的属性。letconst 声明的全局变量不会成为 window 对象的属性,它们只是在全局词法环境中创建独立的绑定。

    var globalVar = "Hello";
    let globalLet = "World";
    function globalFunc() {}
    
    console.log(window.globalVar); // "Hello" (浏览器)
    console.log(window.globalLet); // undefined (浏览器)
    console.log(window.globalFunc); // ƒ globalFunc() (浏览器)

5.2 函数词法环境

  • 特点: 每当调用一个函数时,都会创建一个新的函数执行上下文,随之也创建一个新的函数词法环境。这个环境是声明式环境记录。
  • 内容: 它包含函数的所有参数(作为局部变量)、函数内部声明的 var 变量、let 变量、const 变量以及内部函数声明。
  • outer 引用: 函数词法环境的 outer 引用指向函数定义时所处的词法环境。这正是闭包能够实现的基础。

5.3 块级词法环境 (ES6 let/const)

ES6 引入了 letconst,它们使得 JavaScript 拥有了块级作用域。这意味着在 if 语句、for 循环、while 循环或任何 {} 代码块中声明的 letconst 变量,只在该块内部有效。

  • 当代码执行进入一个 {} 块时,如果块内有 letconst 声明,就会创建一个新的“块级词法环境”。
  • 这个块级词法环境的 outer 引用指向其包含的词法环境。
  • 当块执行完毕,块级词法环境就会被销毁(除非有闭包引用它)。
var i = 10;

for (var i = 0; i < 3; i++) {
  console.log("var loop:", i); // 0, 1, 2
}
console.log("var after loop:", i); // 3 (全局i被修改)

for (let j = 0; j < 3; j++) {
  console.log("let loop:", j); // 0, 1, 2
}
console.log("let after loop:", typeof j); // undefined (j只在循环块内有效)

// 示例:let 的块级作用域
if (true) {
  let blockScopedVar = "I am block scoped";
  console.log(blockScopedVar);
}
// console.log(blockScopedVar); // ReferenceError: blockScopedVar is not defined

5.4 闭包 (Closures)

闭包是 JavaScript 中最强大和最具迷惑性的特性之一,而它正是执行上下文和词法环境交互的直接结果。

定义: 闭包是指一个函数能够记住并访问其外部作用域,即使该外部作用域的函数已经执行完毕并从调用栈中弹出。换句话说,闭包是函数和其周围词法环境的组合。

原理:
当一个内部函数被创建时,它会保留对其创建时所处的外部词法环境的引用(即 outer 引用)。即使外部函数执行完毕,其对应的执行上下文被销毁,但如果内部函数(闭包)仍然存在,并且被外部引用(例如作为返回值或赋值给某个变量),那么内部函数所引用的外部词法环境将不会被垃圾回收器清除,因为它仍然被闭包所“需要”。

代码示例:

function makeAdder(x) {
  // makeAdder 的词法环境创建
  // - 环境记录: { x: 10 }
  // - outer: 指向全局词法环境
  return function (y) {
    // 匿名内部函数的词法环境创建
    // - 环境记录: { y: 5 } (当调用 addFive(5) 时)
    // - outer: 指向 makeAdder 的词法环境
    return x + y; // 访问了 makeAdder 作用域中的 x
  };
}

const addFive = makeAdder(5); // makeAdder 执行完毕,但其词法环境被 addFive 闭包引用
console.log(addFive(10)); // 15
console.log(addFive(20)); // 25

const addTen = makeAdder(10); // 另一个 makeAdder 调用,创建另一个独立的词法环境
console.log(addTen(3)); // 13

在这个例子中:

  • makeAdder(5) 被调用时,它返回了一个匿名函数。
  • 这个匿名函数在 makeAdder 的作用域内定义,因此它的 outer 引用指向了包含 x 变量的 makeAdder 词法环境。
  • 即使 makeAdder(5) 执行完毕,其执行上下文从栈中弹出,但由于 addFive 仍然引用着那个匿名函数,而匿名函数又引用着 makeAdder 的词法环境,所以 x (值为 5) 仍然被保留在内存中。
  • addTen 也是一个闭包,但它引用的是 makeAdder(10) 调用时创建的那个词法环境,其中 x 的值为 10。每个闭包都拥有自己独立的外部词法环境。

闭包的常见应用场景:

  1. 数据私有化 (Encapsulation):创建私有变量和方法。

    function createCounter() {
      let count = 0; // 私有变量
      return {
        increment: function () {
          count++;
          return count;
        },
        decrement: function () {
          count--;
          return count;
        },
        getCount: function () {
          return count;
        },
      };
    }
    
    const counter1 = createCounter();
    console.log(counter1.increment()); // 1
    console.log(counter1.getCount()); // 1
    
    const counter2 = createCounter();
    console.log(counter2.increment()); // 1 (独立的计数器)
    console.log(counter1.getCount()); // 1 (counter1 的计数器未受影响)
  2. 函数柯里化 (Currying):将接受多个参数的函数转换为一系列只接受一个参数的函数。

    function add(a) {
      return function (b) {
        return a + b;
      };
    }
    
    const addTwo = add(2);
    console.log(addTwo(3)); // 5
    console.log(add(5)(7)); // 12
  3. 事件处理器和回调函数:确保回调函数能够访问其定义时的环境。

    // 假设在浏览器环境中
    // for (var i = 1; i <= 3; i++) {
    //   setTimeout(function() {
    //     console.log(i); // 总是输出 4 (因为i是var,全局变量,循环结束后i变为4)
    //   }, i * 1000);
    // }
    
    for (let i = 1; i <= 3; i++) {
      // 每次循环都会创建一个新的块级作用域,let i 绑定到当前值
      setTimeout(function () {
        console.log(i); // 1, 2, 3 (闭包捕获了每次循环的i值)
      }, i * 1000);
    }

第六章:执行上下文栈(Call Stack)的运作

我们已经了解了单个执行上下文的创建和组成。现在,让我们看看多个执行上下文是如何协同工作的,这正是通过执行上下文栈来管理的。

执行上下文栈是一个 LIFO(Last-In, First-Out)结构。

  • 推入栈 (Push):当 JavaScript 代码首次运行时,全局执行上下文被创建并推入栈底。每当调用一个函数时,一个新的函数执行上下文被创建并推入栈顶。
  • 弹出栈 (Pop):当栈顶的函数执行上下文完成执行后(函数返回或抛出错误),它就会从栈中弹出,控制权交还给下一个(下面的)执行上下文。

一个复杂的例子来演示调用栈:

function first() {
  console.log("Entering first EC");
  second();
  console.log("Exiting first EC");
}

function second() {
  console.log("Entering second EC");
  third();
  console.log("Exiting second EC");
}

function third() {
  console.log("Entering third EC");
  let value = 10;
  console.log("Value:", value);
  console.log("Exiting third EC");
  return value;
}

console.log("Global: Before first call");
first();
console.log("Global: After first call");

调用栈的变化过程:

  1. 代码开始执行:

    • 全局执行上下文 (GEC) 被创建并推入栈:[GEC]
    • console.log("Global: Before first call"); 执行。
  2. first() 被调用:

    • first 函数执行上下文 (FEC-first) 被创建并推入栈:[GEC, FEC-first]
    • console.log("Entering first EC"); 执行。
  3. second() 被调用(在 first 内部):

    • second 函数执行上下文 (FEC-second) 被创建并推入栈:[GEC, FEC-first, FEC-second]
    • console.log("Entering second EC"); 执行。
  4. third() 被调用(在 second 内部):

    • third 函数执行上下文 (FEC-third) 被创建并推入栈:[GEC, FEC-first, FEC-second, FEC-third]
    • console.log("Entering third EC"); 执行。
    • let value = 10; 执行。
    • console.log("Value:", value); 执行。
    • console.log("Exiting third EC"); 执行。
    • return value; 执行。
  5. third() 执行完毕:

    • FEC-third 从栈中弹出:[GEC, FEC-first, FEC-second]
    • 控制权返回到 FEC-second。
  6. second() 执行完毕:

    • console.log("Exiting second EC"); 执行。
    • FEC-second 从栈中弹出:[GEC, FEC-first]
    • 控制权返回到 FEC-first。
  7. first() 执行完毕:

    • console.log("Exiting first EC"); 执行。
    • FEC-first 从栈中弹出:[GEC]
    • 控制权返回到 GEC。
  8. 全局代码继续执行:

    • console.log("Global: After first call"); 执行。
  9. 所有代码执行完毕:

    • GEC 从栈中弹出:[]
    • 栈变空。

栈溢出 (Stack Overflow)

如果函数递归调用自己,且没有合适的终止条件,调用栈会不断增长,最终超出其最大容量,导致“栈溢出”错误。

// function infiniteRecursion() {
//   infiniteRecursion(); // 导致栈溢出
// }
// infiniteRecursion();

第七章:综合案例分析:执行上下文全景图

让我们通过一个稍微复杂一些的例子来回顾整个执行上下文的创建和执行过程。

var globalName = "Global Scope";
let globalCounter = 0;

function outerFunction(param1) {
  var outerVar = "I am outer";
  let outerLet = "Outer Let";

  function innerFunction(param2) {
    var innerVar = "I am inner";
    const innerConst = "Inner Const";

    console.log(globalName); // 1
    console.log(globalCounter); // 2
    console.log(outerVar); // 3
    console.log(outerLet); // 4
    console.log(param1); // 5
    console.log(param2); // 6
    console.log(innerVar); // 7
    console.log(innerConst); // 8

    globalCounter++;
    return innerVar + innerConst;
  }

  return innerFunction;
}

const getInner = outerFunction("Hello Param1"); // 调用 outerFunction
const closureFunc = getInner("World Param2"); // 调用 innerFunction (通过闭包)

console.log("Final Global Counter:", globalCounter); // 9

逐步分析:

  1. 初始状态:

    • 执行上下文栈:[]
  2. 加载代码:创建全局执行上下文 (GEC)

    • GEC 被推入栈:[GEC]
    • GEC 创建阶段:
      • this 绑定:window (浏览器) / global (Node.js)
      • 词法环境 (LE_GEC):
        • 环境记录:
          • globalName: undefined (var 提升)
          • globalCounter: uninitialized (let 提升, TDZ)
          • outerFunction: function outerFunction (函数声明提升)
          • getInner: uninitialized (const 提升, TDZ)
          • closureFunc: uninitialized (const 提升, TDZ)
        • outer 引用:null
      • 变量环境 (VE_GEC): (主要处理 var 和函数声明)
        • 环境记录:
          • globalName: undefined
          • outerFunction: function outerFunction
        • outer 引用:null
    • GEC 执行阶段:
      • var globalName = "Global Scope"; -> LE_GEC.globalName = "Global Scope"
      • let globalCounter = 0; -> LE_GEC.globalCounter 离开 TDZ,赋值为 0
      • function outerFunction(...) { ... } (已在创建阶段处理)
      • const getInner = outerFunction("Hello Param1"); -> 调用 outerFunction
  3. 调用 outerFunction("Hello Param1"):创建 outerFunction 执行上下文 (FEC_outer)

    • FEC_outer 被推入栈:[GEC, FEC_outer]
    • FEC_outer 创建阶段:
      • this 绑定:window (默认绑定)
      • 词法环境 (LE_outer):
        • 环境记录:
          • param1: "Hello Param1" (函数参数)
          • outerVar: undefined (var 提升)
          • outerLet: uninitialized (let 提升, TDZ)
          • innerFunction: function innerFunction (函数声明提升)
        • outer 引用:LE_GEC (指向全局词法环境)
      • 变量环境 (VE_outer):
        • 环境记录:
          • param1: "Hello Param1"
          • outerVar: undefined
          • innerFunction: function innerFunction
        • outer 引用:LE_GEC
    • FEC_outer 执行阶段:
      • var outerVar = "I am outer"; -> LE_outer.outerVar = "I am outer"
      • let outerLet = "Outer Let"; -> LE_outer.outerLet 离开 TDZ,赋值为 "Outer Let"
      • return innerFunction; -> innerFunction 被返回。
    • FEC_outer 执行完毕并弹出栈:[GEC]
    • 在 GEC 中,const getInner 离开 TDZ,并赋值为 innerFunction (一个闭包)。
      • LE_GEC.getInner = innerFunction (这个 innerFunction 携带了其定义时的 LE_outer 作为 outer 引用)。
  4. 调用 getInner("World Param2") (实际上是 innerFunction): 创建 innerFunction 执行上下文 (FEC_inner)

    • FEC_inner 被推入栈:[GEC, FEC_inner]
    • FEC_inner 创建阶段:
      • this 绑定:window (默认绑定)
      • 词法环境 (LE_inner):
        • 环境记录:
          • param2: "World Param2" (函数参数)
          • innerVar: undefined (var 提升)
          • innerConst: uninitialized (const 提升, TDZ)
        • outer 引用:LE_outer (指向 outerFunction 的词法环境,即使 outerFunction 已经执行完毕)
      • 变量环境 (VE_inner):
        • 环境记录:
          • param2: "World Param2"
          • innerVar: undefined
        • outer 引用:LE_outer
    • FEC_inner 执行阶段:
      • var innerVar = "I am inner"; -> LE_inner.innerVar = "I am inner"
      • const innerConst = "Inner Const"; -> LE_inner.innerConst 离开 TDZ,赋值为 "Inner Const"
      • console.log(globalName);
        • LE_inner 中查找 globalName:未找到。
        • 沿着 outer 引用到 LE_outer:未找到。
        • 沿着 outer 引用到 LE_GEC:找到 globalName = "Global Scope"。输出。
      • console.log(globalCounter);
        • … 沿着作用域链查找,直到 LE_GEC:找到 globalCounter = 0。输出。
      • console.log(outerVar);
        • LE_inner 中查找:未找到。
        • 沿着 outer 引用到 LE_outer:找到 outerVar = "I am outer"。输出。
      • console.log(outerLet);
        • … 沿着作用域链查找,直到 LE_outer:找到 outerLet = "Outer Let"。输出。
      • console.log(param1);
        • … 沿着作用域链查找,直到 LE_outer:找到 param1 = "Hello Param1"。输出。
      • console.log(param2);
        • LE_inner 中找到 param2 = "World Param2"。输出。
      • console.log(innerVar);
        • LE_inner 中找到 innerVar = "I am inner"。输出。
      • console.log(innerConst);
        • LE_inner 中找到 innerConst = "Inner Const"。输出。
      • globalCounter++; -> 沿着作用域链找到 LE_GEC.globalCounter,更新为 1
      • return innerVar + innerConst; -> 返回 "I am innerInner Const"
    • FEC_inner 执行完毕并弹出栈:[GEC]
    • 在 GEC 中,const closureFunc 离开 TDZ,并赋值为 "I am innerInner Const"
      • LE_GEC.closureFunc = "I am innerInner Const"
  5. GEC 继续执行:

    • console.log("Final Global Counter:", globalCounter); -> 沿着作用域链找到 LE_GEC.globalCounter = 1。输出。
  6. GEC 执行完毕并弹出栈: []

这个详细的步骤展示了执行上下文的创建、变量提升、this 绑定、作用域链查找以及闭包如何保持对外部环境的访问。

第八章:高级考量与细微之处

8.1 eval() 函数的执行上下文

eval() 函数可以执行字符串形式的 JavaScript 代码。它会在调用它的作用域中创建一个新的词法环境,或者在严格模式下创建一个独立的词法环境。由于安全风险(执行任意代码)和性能开销(引擎无法优化 eval 中的代码),应尽量避免使用 eval

8.2 with 语句

with 语句可以扩展一个语句的作用域链。它将一个对象的属性临时添加到当前作用域链的顶部,使得可以直接访问这些属性而无需写对象前缀。然而,with 语句同样不推荐使用,因为它会使得代码难以理解和优化,并且在严格模式下是禁止的。

8.3 模块与它们的词法环境

ES6 模块(import/export)引入了模块级的私有作用域。每个模块都有自己独立的模块级词法环境,与全局环境隔离。模块内部声明的变量和函数默认只在该模块内部可见,除非被 export 导出。这使得模块化开发更加健壮,避免了全局污染。

// moduleA.js
let privateVar = "I am private to moduleA";
export const publicVar = "I am public";

export function getPrivateVar() {
  return privateVar;
}

// app.js
import { publicVar, getPrivateVar } from "./moduleA.js";

console.log(publicVar); // I am public
console.log(getPrivateVar()); // I am private to moduleA
// console.log(privateVar); // ReferenceError: privateVar is not defined

每个模块都有自己的顶级词法环境,其 outer 引用通常指向全局词法环境,但其内部变量和函数不会污染全局对象。

8.4 异步操作与事件循环(Event Loop)

虽然事件循环和任务队列不是执行上下文的直接组成部分,但它们与执行上下文栈紧密协作。当异步操作(如 setTimeout, Promise)的回调函数准备执行时,它们会被放入任务队列。只有当执行上下文栈为空时,事件循环才会将任务队列中的回调函数推入栈中,为它们创建新的函数执行上下文并执行。这是 JavaScript 单线程非阻塞的关键。理解这一点有助于解释为何异步代码的执行顺序有时出乎意料。

第九章:掌握 JavaScript 引擎的基石

通过深入了解 JavaScript 执行上下文的全过程,从变量提升到词法环境的创建,我们揭示了 JavaScript 引擎如何构建和管理代码执行环境的内部机制。这种理解不仅有助于解开 this 关键字、闭包和作用域的谜团,更重要的是,它赋能我们编写更具预测性、更健壮、更易于调试的 JavaScript 代码。掌握这些核心概念,你便能更好地驾驭 JavaScript 这门强大而灵活的语言。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注