调用栈和执行上下文的关系

JavaScript 调用栈如何承载全局与函数执行上下文,以及为什么递归过深会导致栈溢出。

#type / concept #status / growing #tech / dev / frontend #resource / javascript

[!info] related notes

调用栈和执行上下文的关系

一句话定义

调用栈(Call Stack)是 JS 引擎管理执行上下文的 LIFO(后进先出)数据结构,记录当前正在执行的函数及其调用链。

核心机制 / 工作原理

执行上下文类型

类型创建时机特点
全局执行上下文脚本加载时唯一,创建全局对象(window/globalThis)和 this
函数执行上下文函数被调用时每次调用创建一个新的,包含局部变量、参数、作用域链
eval 执行上下文eval() 执行时不推荐使用,有自己的作用域

创建阶段与执行阶段

每个执行上下文分两个阶段:

1. 创建阶段(Creation Phase)

  • 确定 this 绑定
  • 创建变量环境(var 声明提升为 undefined)
  • 创建词法环境(let/const 声明但不初始化,进入暂时性死区 TDZ)
  • 建立作用域链(当前变量环境 + 外部环境引用)

2. 执行阶段(Execution Phase)

  • 逐行执行代码,赋值变量,执行函数调用

调用栈的压栈与弹栈

调用栈状态变化:

1. [全局上下文]                    ← 脚本加载
2. [全局上下文, one 上下文]         ← 调用 one()
3. [全局上下文, one 上下文, two 上下文] ← one 中调用 two()
4. [全局上下文, one 上下文]         ← two 执行完,弹出
5. [全局上下文]                    ← one 执行完,弹出
6. []                             ← 脚本结束,全局上下文弹出

最小例子

function one() {
  two();
  console.log("one");
}

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

one();

执行顺序:

  1. 全局上下文入栈
  2. 调用 oneone 上下文入栈
  3. one 调用 twotwo 上下文入栈
  4. two 执行完出栈 → 输出 "two"
  5. 回到 one 继续执行 → 输出 "one"
  6. one 执行完出栈
  7. 最后回到全局

输出:

two
one

栈溢出示例

function recursive() {
  recursive(); // 无限递归
}

recursive();
// Uncaught RangeError: Maximum call stack size exceeded

V8 默认栈深度约 10000-15000 层(取决于帧大小),超出后抛出 Maximum call stack size exceeded

执行上下文与变量查找

const global = 'global';

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

  function inner() {
    const innerVar = 'inner';
    console.log(global);   // 沿作用域链找到全局
    console.log(outerVar); // 沿作用域链找到 outer
    console.log(innerVar); // 当前上下文
  }

  inner();
}

outer();

inner 的作用域链:inner 变量环境 → outer 变量环境 → 全局变量环境。

边界与常见误解

  • 调用栈 ≠ 作用域链:调用栈管理执行顺序(谁调用谁),作用域链管理变量查找(去哪找变量)。二者独立运行
  • 闭包不在调用栈上:函数返回后其执行上下文从栈上弹出,但变量对象被闭包引用仍在内存中
  • 尾调用优化(TCO):ES6 规范要求尾调用不增加栈深度,但目前仅 Safari 实现了 TCO
  • async/await 的栈:await 暂停时栈会被清空,恢复时重新构建;调试时可能看到”断掉”的调用栈
  • this 绑定在创建阶段确定:不是执行时动态决定,而是进入上下文时就绑定了(箭头函数除外,它继承外层 this
创建于 2026/3/14 更新于 2026/5/27