JavaScript 中的内存管理:栈与堆、引用与深拷贝
在 JavaScript 编程中,理解内存是如何分配和管理的,是掌握语言本质的关键一步。本文将通过两段代码示例,深入探讨 栈内存 与 堆内存 的区别、值拷贝 与 引用拷贝 的行为差异,以及如何实现真正的 深拷贝(Deep Copy) 。
一、第一段代码:引用赋值与共享内存
javascript
编辑
const users = [
{ id: 1, name: "小帅", hometown: "南昌" },
{ id: 2, name: "小刚", hometown: "南昌" },
{ id: 3, name: "小美", hometown: "进贤" }
];
users.push({ id: 4, name: "小刘", hometown: "南昌" });
const data = users; // 引用式赋值
data[0].hobbies = ["篮球", "看烟花"];
console.log(data, users);
1. 堆内存中的动态性
users是一个数组,其内容(对象)存储在 堆内存(Heap) 中。- 堆内存用于存放复杂数据结构(如对象、数组),具有 动态性 和 弹性:可以随时添加、删除元素(如
push操作)。 - JavaScript 引擎不会为堆内存预分配固定大小,而是根据运行时需求动态扩展。
2. 引用赋值的本质
const data = users;并没有复制数组内容,而是将users在堆内存中的 地址 赋给了data。- 因此,
data和users指向同一个堆内存区域。 - 修改
data[0].hobbies实际上修改了共享的对象,所以users也会同步变化。
✅ 结论:对象/数组的赋值是引用传递,不是值拷贝。
二、第二段代码:深拷贝实现真正的独立副本
javascript
编辑
var data = JSON.parse(JSON.stringify(users));
data[0].hobbies = ["篮球", "看烟花"];
console.log(data, users);
1. 为什么需要深拷贝?
当我们希望对数据进行修改但 不影响原始数据 时,必须创建一个 完全独立的副本。这就是深拷贝的作用。
2. JSON.parse(JSON.stringify(...)) 的原理
JSON.stringify(users):将对象序列化为字符串。此过程会遍历整个对象树,生成一个不包含函数、undefined、Symbol等非 JSON 兼容类型的纯数据字符串。JSON.parse(...):将字符串反序列化为新的 JavaScript 对象,该对象在堆内存中拥有 全新的地址空间。
因此,data 和 users 此时 互不影响。
3. 局限性
虽然 JSON 方法简单有效,但它有明显限制:
- 无法处理函数、
Date、RegExp、Map、Set等特殊对象; - 会忽略
undefined和Symbol类型的属性; - 无法处理循环引用(会导致报错)。
✅ 更健壮的深拷贝方案可使用
structuredClone()(现代浏览器支持)或第三方库如 Lodash 的_.cloneDeep()。
三、栈内存 vs 堆内存:对比总结
| 特性 | 栈内存(Stack) | 堆内存(Heap) |
|---|---|---|
| 存储内容 | 基本类型(number, string, boolean 等) | 引用类型(object, array, function) |
| 分配方式 | 自动、连续、固定大小 | 动态、非连续、大小可变 |
| 访问速度 | 快(直接寻址) | 相对较慢(需通过指针) |
| 生命周期 | 随函数调用入栈/出栈自动管理 | 由垃圾回收机制(GC)管理 |
| 示例 | let a = 1; let b = a;(值拷贝) | const arr = []; const ref = arr;(引用共享) |
javascript
编辑
let a = 1;
let b = a; // 值拷贝,b 是 a 的副本,互不影响
四、结语
理解 JavaScript 的内存模型,不仅能帮助我们写出更高效、安全的代码,还能避免常见的“意外修改原始数据”等 bug。关键要点如下:
- 基本类型 存于栈,赋值即拷贝;
- 引用类型 存于堆,赋值仅复制引用;
- 若需独立副本,必须使用 深拷贝;
JSON.parse(JSON.stringify())是简易深拷贝方法,但有局限;- 现代开发中,应根据场景选择合适的拷贝策略。
掌握这些底层机制,你就能在复杂数据操作中游刃有余,写出真正可靠的 JavaScript 程序。