堆内存 vs 栈内存:JavaScript 中的“内存双雄”大对决
引子:你以为你只是在写代码?其实你在玩“内存分配”!
当你写下 let a = 1; 或 const users = [...] 的时候,你可能以为自己只是在定义变量。但其实——你的代码正在悄悄地向计算机申请一块“地盘”,这块地盘要么在栈内存(Stack) ,要么在堆内存(Heap) 。
今天,我们就来一场“内存双雄”的深度剖析,带你搞懂 JavaScript 中堆与栈的本质区别、动态性、引用式拷贝 vs 值拷贝,以及那个让人又爱又恨的深拷贝!
一、栈内存:高效稳定的“公务员”
特点:
- 连续存储
- 读写快如闪电
- 大小固定(基本类型值大小确定,无法动态扩容)
- 存放基本数据类型:
number、string、boolean、undefined、null等 - 对于对象/数组,栈中只存一个引用地址(指针) ,指向堆中的真实数据
示例代码:
let a = 1;
let b = 2;
let c = 3;
let d = a; // 值拷贝,相当于复印了一份
这里,a、b、c、d 都是简单变量,它们的值直接存在栈里。当你执行 d = a,其实是把 1 这个值“复印”了一份给 d。后续无论你怎么改 d,a 都纹丝不动。
💡 小幽默:栈内存就像一个纪律严明的公务员办公室——每个人都有固定工位,不准乱动,效率极高,但想临时加个椅子?门都没有!
二、堆内存:自由奔放的“创业公司”
特点:
- 由垃圾回收器管理,整体布局非连续(但单个对象通常占一块连续空间)
- 支持动态扩容
- 存的是复杂数据结构:数组、对象、函数等
- 变量本身(在栈中)只保存一个地址指针,指向堆中的真实数据
示例代码:
const users = [
{ id: 1, name: '陈', hometown: '江西' },
{ id: 2, name: '徐', hometown: '湖南' },
{ id: 3, name: '王', hometown: '湖北' }
];
// 动态性!堆内存支持弹性扩容
users.push({
id: 4,
name: '赵',
hometown: '广东'
});
看!我们轻松往 users 数组里加了一个新用户。这就是堆内存的动态性——它可以根据需求自动扩展空间,像极了创业公司:人来了就加工位,灵活得很!
🚨 但注意:
users这个变量本身(在栈中)只是一个“门牌号”,真正数据住在堆里的“豪宅”中。
三、引用式拷贝:你以为是复制,其实是“共享账号”
现在问题来了:
const data = users; // 引用式拷贝!
data[0].hobbies = ['篮球', '看烟花'];
console.log(data, users);
输出你会发现:users 也被改了!
为什么?因为 data = users 并没有复制数据,而是把 users 的“门牌号”(堆地址)给了 data。两者指向同一个堆内存对象——就像你和室友共用一个《王者荣耀》账号,你辛辛苦苦练到王者50星,结果他半夜登录,拿你的李白去打人机还送了20个人头……第二天你一上线,战绩惨不忍睹,心态直接炸裂!
💥 这就是引用拷贝的“共享悲剧”:一人操作,全员背锅。
四、深拷贝:真正的“独立户口本”
那怎么才能真正复制一份,互不影响呢?
经典方案:JSON.parse(JSON.stringify(...))
// 先序列化成字符串(脱离原对象),再反序列化成新对象
var data = JSON.parse(JSON.stringify(users));
data[0].hobbies = ['篮球', '看烟花'];
console.log(data, users); // users 不受影响!
这个操作相当于:
- 把堆里的对象“拍成照片”(JSON 字符串)
- 拿这张照片去堆里重新建一栋一模一样的房子(新对象)
data指向新房子,和users再无瓜葛
✅ 优点:简单、有效(对纯 JSON 数据)
❌ 缺点:
- 会丢失函数、
undefined、SymbolDate、RegExp等对象会被转成字符串或空对象- 无法处理循环引用(直接报错)
- 原型链信息全部丢失
🔧 进阶提示:生产环境建议用
lodash.cloneDeep或手写递归深拷贝函数。
五、变量声明的“玄学”:var 与类型推断
var users; // undefined,占栈内存一个小格子
var data; // 同上
// 后面赋值为数组 → 类型由值决定!
users = [ /* ... */ ]; // 此时 users 指向堆内存
JavaScript 是动态类型语言,变量本身没有类型,类型由它当前的值决定。users 最初是 undefined(栈),后来变成对象引用(还是栈,但指向堆)。
🧠 大佬思考题:如果
users被重新赋值为null,它还指向堆吗?
答案:不指向。null是基本类型值,直接存储在栈中,不关联任何堆内存。
六、总结:一张表看懂堆 vs 栈
| 特性 | 栈内存(Stack) | 堆内存(Heap) |
|---|---|---|
| 存储内容 | 基本类型的值、对象/数组的引用地址(指针) | 对象、数组、函数等复杂数据结构 |
| 内存分配时机 | 函数调用时自动分配,作用域结束自动释放 | 运行时动态分配,由垃圾回收器管理 |
| 访问速度 | 极快(连续内存 + 直接访问) | 较慢(需通过指针跳转) |
| 生命周期 | 随函数调用结束而销毁 | 不确定,依赖引用关系和 GC(垃圾回收) |
| 拷贝行为 | 值拷贝(独立副本) | 默认引用拷贝(共享地址) |
| 适合场景 | 临时变量、函数参数、基本类型操作 | 大型数据、需要动态增删改的结构 |