Skip to content

第1章:栈帧(Stack Frame)—— 函数调用的物理载体

this 不是魔法,它是栈帧中的一个字段。”
—— 当你看到 receiver,你就看穿了 this 的本质。

核心概念

1. 调用栈(Call Stack)

  • 定义:一个后进先出(LIFO)的栈结构,用于管理函数的调用顺序。
  • 作用:确保函数执行完后能正确返回到调用点。
  • 类比:像一摞盘子,最后放上去的最先被拿走。

2. 栈帧(Stack Frame)

  • 定义:调用栈中的每一个单元,对应一次函数调用。
  • 生命周期:函数被调用时创建(push),函数返回时销毁(pop)。
  • 存储位置栈内存(Stack Memory),分配和释放极快。

栈帧里有什么?—— V8 引擎的“驾驶舱”

当 JavaScript 引擎(如 V8)执行一个函数时,它会在调用栈上创建一个栈帧。这个帧不是一个简单的盒子,而是一个包含多个关键字段的结构体。

以下是 V8 中栈帧的简化 C++ 实现模型

cpp
class StackFrame {
 public:
  Object* function;        // 当前正在执行的函数对象
  Object* receiver;        // 就是 this!函数调用时的 this 值
  Address argv;            // 参数地址(arguments vector)
  Address return_addr;     // 返回地址:函数执行完后,该跳回哪一行代码
  Context* context;        // 指向当前词法环境的 Context 对象(可选)
  // ... 其他运行时元信息(如调试信息、优化标记等)
};

真实代码示例:函数调用链

js
function a() { 
  console.log("a 开始");
  b(); 
  console.log("a 结束");
}

function b() { 
  console.log("b 开始");
  c(); 
  console.log("b 结束");
}

function c() { 
  console.log("c 执行中"); 
  // 此时 c 是当前执行函数
}

a(); // 启动调用

执行到 c() 时,调用栈长这样:

┌─────────────────────────────┐ ← 栈顶(Top of Stack)
│ [c() 的栈帧]                 │
│ function: c                  │
│ receiver: window (或 global) │
│ argv: []                     │
│ return_addr: b 的下一行      │
│ context: → Context_C         │
├─────────────────────────────┤
│ [b() 的栈帧]                 │
│ function: b                  │
│ receiver: window             │
│ argv: []                     │
│ return_addr: a 的下一行      │
│ context: → Context_B         │
├─────────────────────────────┤
│ [a() 的栈帧]                 │
│ function: a                  │
│ receiver: window             │
│ argv: []                     │
│ return_addr: 全局下一行      │
│ context: → Context_A         │
├─────────────────────────────┤
│ [全局执行上下文]              │ ← 栈底(Bottom)
│ (main)                       │
└─────────────────────────────┘

关键字段详解

字段说明
function当前执行的函数对象。引擎知道“现在在跑谁”
receiver这就是 this 的物理存在!
谁调用这个函数,receiver 就指向谁
argv函数参数的地址。引擎通过它读取 arguments
return_addr记录“从哪来回哪去”。
c() 执行完,引擎根据它跳回 b() 的下一行
context指向堆上的 Context 对象,存储 let/const/var 变量

为什么这很重要?

1. this 的真相

  • this 是动态的” → 错!
  • thisreceiver 字段的值” → 对!

当你写:

js
obj.method();

V8 实际做的是:

cpp
// 创建栈帧
frame.receiver = obj;  // 把 obj 赋给 receiver
frame.function = method;
// 推入调用栈
stack.push(frame);

所以 this 的绑定,发生在栈帧创建时,是静态的物理操作

2. 调用栈的“导航系统”

  • return_addr 让引擎知道“下一步去哪”
  • 这就是为什么错误堆栈能精确显示“at 哪个文件哪一行”
  • 没有它,程序就会“迷路”,无法返回

3. 性能优势:栈内存极快

  • 栈帧分配在栈内存,只需移动栈指针
  • 比堆内存分配快几个数量级
  • 函数频繁调用/返回,依赖的就是这种高效机制

注意:栈帧 ≠ 所有变量的家

  • 栈帧本身只存:
    • this(receiver)
    • 函数指针
    • 返回地址
    • 指向 Context 的指针
  • 真正的变量let x = 1)存在堆上的 Context 对象
  • 栈帧只是“入口”,通往变量的“门牌号”

本章核心结论

栈帧是函数调用的物理载体,是执行上下文中 ThisBinding 的真实实现。

  • this 不是计算出来的,它是 receiver 字段的值。
  • 调用栈不是抽象概念,它是内存中真实存在的 LIFO 结构。
  • 每一次函数调用,都是在栈上“盖一栋楼”;每一次返回,都是“拆掉一层”。