在 JavaScript 开发中,我们常常听说"原始类型存在栈里,对象存在堆里",但很少有人深入探讨:函数代码存在哪里?执行时到底发生了什么?为什么闭包能记住变量?
要真正理解这些问题,必须跳出"栈 vs 堆"的二元模型,引入第三个关键角色------代码空间(Code Space) 。本文将系统阐述 JavaScript 引擎(以 V8 为例)中 代码空间、栈空间、堆空间 三者如何协同工作,共同支撑起整个语言的运行机制。
一、三大内存空间的角色定位
JavaScript 引擎在运行时,会将内存划分为三个逻辑区域,各司其职:
| 空间 | 存储内容 | 特点 |
|---|---|---|
| 代码空间 | 编译后的字节码或机器码 | 只读、共享、由引擎管理 |
| 栈空间 | 执行上下文、原始值、对象引用 | 快速分配/释放、连续、生命周期短 |
| 堆空间 | 所有对象(包括函数对象、闭包环境) | 动态分配、不连续、依赖垃圾回收 |
这三者并非孤立存在,而是通过指针引用 和执行流紧密协作。
二、函数定义:三空间的初次联动
考虑以下函数定义:
css
function add(a, b) {
return a + b;
}
1. 代码空间:存放执行逻辑
- 引擎将
return a + b编译为字节码(如Load a,Load b,Add,Return); - 这些指令被存入代码空间,作为可复用的执行模板。
2. 堆空间:创建函数对象
-
JavaScript 中函数是一等对象 ,因此会创建一个
Function类型的对象; -
该对象包含:
- 属性:
name: "add",length: 2; - 内部指针
[[Code]]:指向代码空间中add的字节码入口; [[Environment]]:指向定义时的词法环境(用于闭包)。
- 属性:
3. 栈空间:注册变量引用
- 在全局执行上下文中,变量
add被注册; - 其值是一个引用(指针) ,指向堆中的函数对象。
此时三空间已建立连接:
栈(add 变量) → 堆(函数对象) → 代码空间(字节码)
三、函数调用:三空间的动态协作
执行 const result = add(2, 3); 时,三空间开始动态交互:
步骤 1:查找函数入口
- 引擎通过栈中的
add变量,找到堆中的函数对象; - 从函数对象取出
[[Code]]指针,定位到代码空间的起始指令。
步骤 2:压入执行上下文(栈空间)
-
在调用栈顶部创建新帧,包含:
- 参数
a = 2,b = 3(原始值,直接存栈); - 返回地址、作用域链等控制信息。
- 参数
步骤 3:执行指令(引擎驱动)
-
引擎(如 V8 的 Ignition 解释器)逐条读取代码空间中的字节码,并在 CPU 上执行:
Load a→ 从当前栈帧读取2;Load b→ 读取3;Add→ 引擎内部计算2 + 3 = 5(这是 C++ 实现的底层操作);Return→ 准备返回值。
注意:执行发生在引擎/CPU 层面,不是在栈或堆中 。
栈和堆只是被操作的"数据区"。
步骤 4:返回结果(写回栈)
- 返回值
5(原始类型)被写入调用者的栈帧 (即result = 5); add的栈帧被弹出,参数a、b自动销毁。
四、复杂场景:对象与闭包如何影响三空间
场景 1:返回对象
ini
function createObj(x) {
return { value: x };
}
const obj = createObj(10);
{ value: x }是引用类型 → 在堆空间分配内存;- 栈中
obj变量存储的是堆地址(引用) ; - 即使
createObj执行结束,只要obj存在,堆中对象就不会被 GC 回收。
场景 2:闭包捕获变量
ini
function outer(n) {
let count = n;
return function inner() {
return ++count;
};
}
const inc = outer(5);
inc(); // 6
count原本是outer的局部变量(应存栈中);- 但
inner引用了它 → 引擎检测到闭包; - 于是将
count从栈提升到堆中 ,放入一个特殊的 closure 环境对象; inner的[[Environment]]指向该堆对象;- 即使
outer栈帧销毁,count仍存活于堆中。
闭包的本质:通过堆空间延长变量生命周期,打破栈的自动回收规则。
五、总结
执行流程: 引擎 → 读代码空间指令 → 操作栈(取参数)和堆(建对象) → 结果写回调用者栈帧
- 代码空间:提供"做什么"(指令);
- 栈空间:提供"用什么"(临时数据);
- 堆空间:提供"存什么"(持久对象);
- JavaScript 引擎:负责"怎么做"(执行协调)。
理解这三者的分工与协作,你就能真正看透 JavaScript 的运行本质------无论是性能优化、内存泄漏排查,还是闭包、异步、模块化等高级特性,都将变得清晰而自然。
掌握内存,方能驾驭语言。