栈空间和堆空间
约 1045 字大约 3 分钟
2026-02-26
V8 在执行 JavaScript 时,会把 "执行上下文状态" 和 "对象实体数据" 放在不同存储区域。这就是理解内存模型时最常用的两个概念:栈空间 与 堆空间
| 存储区域 | 主要存什么 | 典型特征 |
|---|---|---|
| 栈(Call Stack) | 执行上下文、局部变量、返回地址、引用地址等 | 空间较小,分配/回收快,先进后出 |
| 堆(Heap) | 对象、数组、函数对象、闭包环境等复杂数据 | 空间较大,分配灵活,由垃圾回收器管理 |
栈与堆的分配过程
示例代码
详情
function foo() { var a = '极客时间' var b = a var c = { name: '极客时间' } var d = c } foo()创建函数执行上下文并入栈
JS 引擎先编译再执行。
foo()调用发生时,会创建foo的执行上下文,包含变量环境、词法环境,并压入调用栈原始值直接写入当前栈帧
a保存字符串值,b复制的是a当前的值
调用栈状态图 对象分配
执行
var c = { name: '极客时间' }时:- 对象实体在堆中申请空间
- 假设得到一个地址(如
1003) - 变量
c在栈帧中存的是这个地址

使用堆内存存储对象类型 复制引用地址
var d = c不是再创建一个对象,而是把c的地址再拷贝一份给d。c和d指向同一个堆对象执行结束
foo返回后,它的执行上下文会从调用栈弹出。若堆中对象已无可达引用,后续会由垃圾回收器回收为什么不能把所有数据都放在栈里
栈是为“高频上下文切换”设计的,核心目标是快进快出。 如果把大量复杂对象都放进栈,会让函数切换与回收成本显著上升,直接影响执行效率。

调用栈切换上下文状态
再谈闭包
第一次接触闭包时可能会困惑:外层函数都执行完了,为什么变量还没被销毁?核心原因是:仍然有内部函数在引用这些变量
示例代码
详情
function foo() { var myName = '极客时间' let test1 = 1 const test2 = 2 var innerBar = { setName: function (newName) { myName = newName }, getName: function () { console.log(test1) return myName }, } return innerBar } var bar = foo() bar.setName('极客邦') bar.getName() console.log(bar.getName())创建执行上下文
此时变量会先按常规规则进入当前作用域管理结构
识别外部引用
执行流程:
- 编译过程中,遇到内部函数
setName,因此 JS 引擎还需要对内部函数做一次快速的词法扫描,发现内部函数引用了外部myName变量。所以 JS 引擎判断此处是一个闭包,于是会在堆空间创建一个外部无法访问的对象closure(foo),用于保存myName变量 - 接着继续编译,遇到内部函数
getName,发现内部引用了外部变量test1,于是 JS 引擎又将test1添加到closure(foo)对象中 - 由于
test2没有被内容函数引用,所以依然将会保存在调用栈中
- 编译过程中,遇到内部函数
return innerBar当最终代码执行到
return innerBar时,堆栈调用如下
但
bar仍然持有setName/getName,而它们又持有对闭包变量的引用, 所以myName、test1不会随着foo返回而立即释放。结束调用
foo 执行结束之后,返回的
getName和setName都引用了closure(foo)对象,因此即使 foo 函数执行完毕closure(foo)对象依然被引用在下次调用这两个函数时,创建的执行上下文中就会包含
closure(foo)对象,从而实现闭包何时才会真正释放
当
bar及其内部函数都不可达时,这部分闭包数据才会在后续 GC 中被回收
