技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

「前端进阶」JS 中的栈内存与堆内存

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/64-「前端进阶」JS中的栈内存堆内存
本文目录8 个章节
  1. 一、先区分两个“栈/堆”概念
  2. 二、JavaScript 的值和对象身份
  3. 三、为什么“基本类型在栈、对象在堆”只是模型
  4. 四、变量复制的直观例子
  5. 五、函数参数不是“引用传递”
  6. 六、闭包和执行环境
  7. 七、内存分配、回收和性能
  8. 八、总结

「前端进阶」JS 中的栈内存与堆内存

“基本类型在栈、引用类型在堆”是前端文章中常见的入门模型,但它不是 ECMAScript 规范规定的存储布局。本文保留这个模型用于建立直觉,同时补充现代 JavaScript 引擎的真实边界:规范描述值、对象身份、执行上下文和可达性,具体放在栈、堆、寄存器还是优化后的表示由引擎决定。

一、先区分两个“栈/堆”概念

1. 栈(stack)数据结构

栈是一种后进先出(LIFO,last-in-first-out)的数据结构,只能从栈顶访问。把乒乓球放进盒子可以帮助理解:最后放进去的球最先取出,想拿底部的球必须先移走上面的球。

栈数据结构示意图

图片来源:原文配图。已下载为本地图片;原图来源站点的版权和许可信息仍然适用。

2. 堆(heap)数据结构

在数据结构课程中,heap 通常指二叉堆等优先队列结构,它和“堆内存”不是同一个概念:

  • 二叉堆有父子关系,常用于实现最小堆或最大堆;
  • 堆内存是运行时管理动态对象的一种实现区域;
  • “堆内存可以随机访问”是对动态存储的口语描述,不代表所有访问都有相同成本。

把这两个概念混为一谈,会误以为 JavaScript 的对象一定存储在某种二叉堆中,这是错误的。

二、JavaScript 的值和对象身份

ECMAScript 规定了原始值和对象值,而没有规定必须使用栈和堆保存它们。现代规范中的原始类型包括:

  • undefined
  • null
  • Boolean
  • Number
  • BigInt
  • String
  • Symbol

除此之外是对象类型,包括普通对象、数组、函数、日期、正则、Map、Set、TypedArray 等。

原始值不可变,但保存原始值的变量可以重新绑定:

let value = 'hello'
value += ' world'
console.log(value) // hello world

这里不是修改了原来的字符串,而是计算出一个新字符串并让变量绑定到新值。引擎是否复用字符串、是否把某个短值直接编码在寄存器或指针中,属于实现细节。

原始值存储示意图

图片来源:原文配图

对象具有身份。两个结构相同的对象也不相等,因为它们是不同的对象值:

const a = { name: 'Ada' }
const b = { name: 'Ada' }
const c = a

console.log(a === b) // false
console.log(a === c) // true

变量复制时,复制的是对应的值;当值是对象时,这个值表现为对同一对象身份的引用:

let m = { a: 10, b: 20 }
let n = m
n.a = 15

console.log(m.a) // 15
console.log(m === n) // true

对象引用示意图

图片来源:原文配图

三、为什么“基本类型在栈、对象在堆”只是模型

经典模型为了方便讲解,常画成:

  • 变量保存原始值,原始值画在栈中;
  • 变量保存对象引用,对象本体画在堆中;
  • 函数调用创建栈帧,调用结束后栈帧消失;
  • 堆中的对象在不再可达时等待垃圾回收。

这个模型可以帮助理解“对象赋值后会共享修改”,但以下说法不能当作规范事实:

  • 所有原始值都一定放在栈内存;
  • 所有对象都一定放在堆内存;
  • 栈内存大小固定、堆内存一定更慢;
  • 闭包变量必然被搬到堆上;
  • 变量离开函数就立刻销毁。

JavaScript 引擎可能使用指针压缩、内联小整数、寄存器、逃逸分析、标量替换和优化后的对象表示。闭包捕获的变量可能被提升到引擎管理的环境记录中,也可能被优化;只要仍可观察,开发者不应依赖其物理地址。

四、变量复制的直观例子

4.1 原始值复制

let a = 20
let b = a
b = 30

console.log(a) // 20
console.log(b) // 30

这是“值相互独立”的语言层表现。不要把它简单理解为一定创建了两个固定大小的栈槽;实现可以共享不可变数据或采用立即数表示,只要可观察结果符合规范即可。

原始值复制示意图

图片来源:原文配图

4.2 对象引用复制

const first = { name: 'Ada' }
const second = first
second.name = 'Grace'

console.log(first.name) // Grace
console.log(first === second) // true

如果想创建新的外层对象,需要显式浅拷贝或深拷贝:

const source = { name: 'Ada', profile: { language: 'JavaScript' } }
const shallow = { ...source }

shallow.name = 'Grace'
shallow.profile.language = 'TypeScript'

console.log(source.name) // Ada
console.log(source.profile.language) // TypeScript,共享嵌套对象

引用复制示意图

图片来源:原文配图

五、函数参数不是“引用传递”

ECMAScript 的函数参数是按值传递。对象参数传递的是对象引用这个值的副本:

let name = 'ConardLi'
function changeName(value) {
  value = 'code秘密花园'
}

changeName(name)
console.log(name) // ConardLi

修改对象属性可以观察到外部变化,是因为两个引用值指向同一个对象:

const user = { name: 'ConardLi' }

function changeUser(value) {
  value.name = 'code秘密花园'
  value = { name: 'another user' }
}

changeUser(user)
console.log(user.name) // code秘密花园

函数内部重新绑定 value 不会重新绑定外部的 user。更准确的术语是“按值传递对象引用”,而不是“按引用传递变量”。

六、闭包和执行环境

原文的闭包示例存在 functionAfunctionBAB 大小写不一致的问题。可运行示例:

function createCounter() {
  let count = 0

  return function readAndIncrement() {
    count += 1
    return count
  }
}

const next = createCounter()
console.log(next()) // 1
console.log(next()) // 2

readAndIncrement 仍然可以访问 createCounter 的词法环境,所以形成闭包。调用栈中的函数调用记录可以结束,但只要闭包仍然可达,被捕获的 count 就不能被回收。

这并不意味着开发者可以精确断言“闭包变量存储在堆内存”。引擎可能使用不同的环境表示和优化;从应用角度只需要关注引用是否仍然可达,以及监听器、定时器、缓存是否意外保留闭包。

七、内存分配、回收和性能

经典文章常把栈说成效率高、堆说成效率低。实际性能取决于对象大小、分配器、缓存局部性、垃圾回收压力、引擎优化和代码形态,不能用“栈一定快、堆一定慢”作性能结论。

可以关注这些更可靠的实践:

  • 不要在循环中无必要地创建大量临时对象;
  • 不要通过全局数组、事件监听器、定时器或无限缓存长期保留已不用的数据;
  • 使用浏览器 Performance/Memory 工具和 Node.js heap snapshot 验证泄漏;
  • 不要为了“让对象进栈”而改变代码,JavaScript 不提供这样的可移植控制;
  • 对需要结构复制的场景,明确使用浅拷贝、structuredClone() 或领域序列化,而不是依赖内存布局。

垃圾回收通常依据可达性判断对象是否仍可能被程序访问。对象即使不可达,也不会保证在某个固定时刻立刻释放;GC 的触发和回收时机由引擎决定。

八、总结

  1. 栈和堆既可以指数据结构,也可以指运行时内存区域,不能混为一谈;
  2. ECMAScript 规定原始值、对象身份和引用语义,没有规定值必须放在栈或堆;
  3. 原始值不可变,但变量可以重新绑定;对象赋值会复制对象引用值;
  4. 函数参数始终按值传递,对象参数只是传递了引用值的副本;
  5. 闭包会保留仍被使用的词法环境,但不能据此断言具体物理存储位置;
  6. 内存问题应从可达性、生命周期和实际快照分析,而不是只背“栈堆模型”。

参考资料:

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS