计算机内存中的栈和堆
一、堆栈的定义
程序运行时,内存会划分出不同区域,其中最重要的两块就是栈(Stack)和堆(Heap)。
1. 栈(Stack)
- 结构:一种线性的、后进先出(LIFO)的数据结构。
- 管理方式:由操作系统或 JavaScript 引擎自动分配和释放,无需开发者手动干预。比如函数调用时,局部变量入栈;函数执行完,整块栈帧直接弹出,内存瞬间回收。
- 特点:
- 速度快,直接通过移动栈指针来分配。
- 空间相对较小,连续存储。
- 存储大小固定、生命周期可预知的数据。
2. 堆(Heap)
- 结构:更像一个杂乱但自由的大仓库,存储区域不一定连续,通过指针引用。
- 管理方式:动态分配。何时分配、何时释放并不严格跟随函数进出,而是由垃圾回收器(GC)通过算法判断对象是否还被使用,来决定回收时机。
- 特点:
- 速度较慢,涉及内存查找与 GC。
- 空间大,适合存放大小不定或需要长期存在的数据。
打个比方:栈像便捷的随身口袋,放快取快,但容量小;堆像家里的储物间,空间大但找东西(及收拾)要花时间。
二、JavaScript 的数据类型
JavaScript 的值分为两大阵营:
原始类型(Primitive)
undefinednullbooleannumberbigintstringsymbol
它们的特点:不可变,大小相对固定(除了某些长字符串,但引擎有优化)。
对象类型(Reference / Object)
Object、Array、Function、Date、RegExp、Map、Set等。- 特点:可变,结构复杂,大小动态增减。
三、存储地方:栈与堆的“分工”
1. 基本数据类型的存储
绝大多数情况下,原始类型的值直接存在栈中。
- 变量声明时,引擎会在栈中划分一块区域,把值直接放进去(比如数字
10,布尔true)。 - 栈变量本身持有这个原始值,不经过任何中间地址。
⚠️ 有一些“特殊”的原始值,如较长的字符串,引擎内部可能会把它们放进堆,而在栈上只存一个引用。这属于引擎优化,但从逻辑语义上,我们仍把字符串当作原始类型,赋值时依然会复制,行为上等同于直接存值。
2. 对象数据类型的存储
对象的数据实体始终存放在堆内存中,而栈中只存储一个“门牌号”——即堆内存的引用地址。
- 当你写
let obj = { name: 'Alice' };,引擎会做两件事:- 在堆中分配一块内存存放对象
{ name: 'Alice' },得到一个地址(比如0x003FFF)。 - 在栈中给变量
obj分配空间,里面存的值正是这个地址0x003FFF。
- 在堆中分配一块内存存放对象
访问obj.name时,先通过栈里的地址找到堆中的对象,再取出其属性。
这种分工的原因在于:对象大小可能在运行时随时变化(增减属性),把这种不稳定的数据放在容量有限、空间连续的栈里很危险。而栈只需保存一个固定大小的内存地址即可。
四、存储方式:按值 vs 按引用
这是区分原始类型和对象类型最核心的行为差异。
1. 原始类型 —— 按值存储和复制
操作直接对“值”本身进行,互不影响。
leta=10;letb=a;// 把 a 的值 10 复制一份给 bb=20;console.log(a);// 10,a 不变a 和 b 在栈中有各自独立的空间,改一个不会影响另一个。
2. 对象类型 —— 按引用存储和复制
变量保存的是“引用地址”,复制时只复制地址,而不在堆里复制整个对象。
letobj1={name:'Alice'};letobj2=obj1;// 复制的是地址,obj2 也指向堆里同一个对象obj2.name='Bob';console.log(obj1.name);// 'Bob',obj1 跟着变了obj1 和 obj2 里的地址相同,因此通过任何一个变量修改对象,都会反映在另一个上。
函数的参数传递也遵守同样规则:
- 传原始类型:函数内部修改形参不会影响外部实参。
- 传对象:修改对象属性会影响外部,因为内外指向同一个对象。
functionchange(num,obj){num=100;// 改的是局部副本obj.name='Zoe';// 改的是堆里的同一对象}letn=1;leto={name:'Tom'};change(n,o);console.log(n);// 1,未变console.log(o.name);// 'Zoe',变了五、为什么要这样设计——深度理解
性能与效率
栈分配只需移动指针,极快。原始类型大小已知、占用固定,适合栈。对象如果直接放在栈中,其动态扩展会引起大量内存搬移;放在堆中,栈上只保留一个指针,保持高效。生命周期管理
局部变量一般随函数退出销毁,栈自动弹出。对象却可能被多个变量引用、逃逸出当前作用域,堆配合垃圾回收能处理复杂存活分析。内存布局与安全性
栈连续、有序,容易产生栈溢出;堆则分散。把不确定性大的对象放堆,也让栈维持轻量安全。
六、进阶:那些“纯粹”之外的存储情况
实际引擎(如 V8)会优化存储,比如:
- 小整数(Smi):可能直接编码在指针里,避免额外分配。
- 字符串:字符串常量池,可能会在堆中存储,但逻辑上仍是不可变的原始类型。复制字符串时,通常表现为复制引用(写时复制),但对开发者透明,行为仍符合“按值传递”。
- 闭包引用的变量:如果函数引用了外部原始类型变量,这个变量会被移到堆中(放在“环境”对象里),从而延长生命周期。这时原始值确实存在了堆里,但从语言视角它还是“原始值”,只是存在位置变成了堆。
这些实现细节不影响我们理解的大原则:原始类型表现为“值语义”,对象类型表现为“引用语义”;栈负责快速管理大小固定的值或地址,堆负责承载复杂动态的数据实体。
七、总结
- 堆栈定义:栈是自动分配释放、有序快速的小容量区域;堆是动态分配、靠 GC 回收的大容量区域。
- 存储地方:基本数据类型直接存于栈(值本身);对象实体存于堆,栈里只留堆地址。
- 存储方式:基本类型按值复制,对象类型按引用复制。
- 设计意图:兼顾性能与灵活性,让小而短命的数据速生速灭,大而长命的数据由堆和垃圾回收精细管理。
理解这一模型后,JavaScript 里赋值、传参、比较(===对对象比较地址)等常见行为就都有了坚实的底层基础。
