JavaScript 执行上下文

深入解析 JavaScript 执行上下文的创建过程、变量环境与词法环境的区别、作用域链的形成机制,以及闭包与执行上下文的内在联系。

为什么执行上下文是 JavaScript 运行时的核心

当你写下一段 JavaScript 代码并让它运行,JavaScript 引擎并不是逐行解析执行那么简单。在代码真正执行之前,引擎会先创建一个执行上下文(Execution Context),这是代码运行时的环境容器。理解执行上下文,才能理解变量提升、作用域链、闭包、this 绑定的底层机制。

这篇文章会让你看到 JavaScript 引擎在执行代码之前做了什么,以及为什么某些看似奇怪的行为(如 var 变量提升、let 的暂时性死区)其实是引擎设计的有意之举。


一、执行上下文的本质

1.1 什么是执行上下文

执行上下文是 JavaScript 引擎管理代码执行的一种机制。每个执行上下文包含:

  1. 变量环境(VariableEnvironment):存储 var 声明和函数声明
  2. 词法环境(LexicalEnvironment):存储 letconst、模块作用域
  3. this 绑定(ThisBinding):决定 this 关键字的值
  4. 外部环境引用(Outer Environment Reference):形成作用域链

1.2 三种执行上下文

flowchart TD
    A["执行上下文栈<br/>Call Stack"] --> B["全局执行上下文<br/>Global EC"]
    B --> C["函数执行上下文<br/>Function EC"]
    C --> D["Eval 执行上下文<br/>Eval EC"]

全局执行上下文:代码首次运行时的默认上下文。在浏览器中,全局上下文关联到 window 对象;在 Node.js 中,关联到 global 对象。

函数执行上下文:每次调用函数时创建。函数不直接创建上下文——调用时才创建。

Eval 执行上下文:不推荐使用,eval 代码在特殊上下文中运行。


二、执行上下文栈(Call Stack)

2.1 栈的工作原理

JavaScript 是单线程语言,通过栈结构管理执行上下文的嵌套关系:

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

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

function third() {
  console.log("third");
}

first();

执行顺序分析:

栈状态变化:

1. first() 被调用
   Stack: [Global EC, first() EC]

2. first() 内部调用 second()
   Stack: [Global EC, first() EC, second() EC]

3. second() 内部调用 third()
   Stack: [Global EC, first() EC, second() EC, third() EC]

4. third() 执行 console.log("third"),然后返回
   Stack: [Global EC, first() EC, second() EC]

5. second() 执行 console.log("second"),然后返回
   Stack: [Global EC, first() EC]

6. first() 执行 console.log("first"),然后返回
   Stack: [Global EC]

输出顺序:third → second → first

2.2 栈溢出(Stack Overflow)

递归调用如果没有终止条件,会不断压栈直到超出限制:

function infinite() {
  infinite();
}
infinite(); // RangeError: Maximum call stack size exceeded

现代浏览器和 Node.js 的栈大小大约在 1-2 万层左右。


三、变量环境 vs 词法环境

3.1 两者对比

特性 变量环境(VariableEnvironment) 词法环境(LexicalEnvironment)
创建时机 函数调用时 函数/模块加载时
存储内容 var 声明、函数声明 letconst、模块作用域
提升行为 完全提升(声明和赋值都提升) 仅声明提升,赋值留在原地
块级作用域 不支持 支持({} 块)

3.2 ECMAScript 规范定义

根据 ECMA-262 规范,执行上下文由以下组件构成:

ExecutionContext = {
  VariableEnvironment: { // var 和函数声明
    envRecord: { ... },
    outer: <outer reference>
  },
  LexicalEnvironment: { // let, const, 模块作用域
    envRecord: { ... },
    outer: <outer reference>
  },
  ThisBinding: <this value>
}

3.3 代码验证

function test() {
  console.log(a); // undefined — var 提升
  console.log(b); // ReferenceError — let 的 TDZ

  var a = 1;
  let b = 2;
}
test();

四、作用域链(Scope Chain)

4.1 作用域链的形成

当代码访问变量时,引擎沿作用域链从内向外查找:

const global = "global";

function outer() {
  const outerVar = "outer";

  function inner() {
    const innerVar = "inner";
    console.log(innerVar); // 自身
    console.log(outerVar); // 从 outer 作用域找到
    console.log(global);   // 从全局作用域找到
  }

  inner();
}

outer();
flowchart TD
    subgraph Global["全局作用域"]
        G["global = 'global'"]
    end

    subgraph outer_EC["outer 执行上下文"]
        O["outerVar = 'outer'"]
    end

    subgraph inner_EC["inner 执行上下文"]
        I["innerVar = 'inner'"]
    end

    I -->|outer| O
    O -->|outer| G

4.2 外部环境引用的设定时机

外部环境引用在函数定义时确定,而非调用时:

function createAdder(x) {
  return function(y) {
    return x + y; // x 从 createAdder 的作用域捕获
  };
}

const add5 = createAdder(5);
console.log(add5(10)); // 15

createAdder 执行完毕后,其执行上下文应该弹出栈,但因为内部函数仍然引用 x,所以 createAdder 的执行上下文不会被完全销毁——这就是闭包的雏形。


五、闭包的本质

5.1 闭包的定义

闭包是指函数能够记住并访问其词法作用域中的变量,即使该函数在其词法作用域之外执行。

5.2 闭包的实现机制

当函数被创建时,函数有一个内部属性 [[Environment]](规范中的内部槽),指向创建时的词法环境。当函数执行时,新的执行上下文的外部环境引用被设置为函数的 [[Environment]]

function outer() {
  const x = 10;

  function inner() {
    console.log(x); // 访问 outer 中的 x
  }

  return inner;
}

const fn = outer();
fn(); // 10 — outer 的执行上下文虽然已弹出栈,
       // 但 inner 的 [[Environment]] 仍然引用着它

5.3 V8 的优化:上下文分配

V8 会分析变量是否被内部函数引用。只有被引用的变量才会被分配到堆上的 Context 对象中,未被引用的变量仍然在栈上,函数结束后自动销毁。

function optimized() {
  const used = 1;    // 被闭包引用 → 分配到堆
  const unused = 2;  // 未被引用 → 栈上分配,执行后销毁

  return function() {
    return used;
  };
}

六、面试高频问题

Q: 什么是执行上下文?

执行上下文是 JavaScript 引擎管理代码执行的抽象概念。每次代码运行(全局代码、函数调用、eval)都会创建一个执行上下文。执行上下文包含变量环境、词法环境、this 绑定和外部环境引用。JavaScript 通过执行上下文栈管理多个上下文的嵌套关系。

Q: 变量提升的原理是什么?

变量提升源于 JavaScript 引擎的两阶段处理:第一阶段是创建阶段,引擎扫描代码并将 var 声明和函数声明加入变量环境;第二阶段是执行阶段,按照代码顺序执行。var 的声明和赋值都会被提升,但赋值留在原地,所以提升后访问是 undefined。函数声明整体提升,可以在声明前调用。

Q: 闭包是如何形成的?

闭包的形成需要三个条件:函数嵌套、内部函数引用外部变量、内部函数在外部函数执行完毕后仍可达。当函数创建时,其 [[Environment]] 内部槽记录创建时的词法环境。只要函数对象存在,这个词法环境就不会被垃圾回收,即使创建它的外部函数已经执行完毕。


延展阅读