JavaScript 中的内存管理:栈与堆、引用与深拷贝

53 阅读3分钟

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) :将对象序列化为字符串。此过程会遍历整个对象树,生成一个不包含函数、undefinedSymbol 等非 JSON 兼容类型的纯数据字符串。
  • JSON.parse(...) :将字符串反序列化为新的 JavaScript 对象,该对象在堆内存中拥有 全新的地址空间

因此,datausers 此时 互不影响

3. 局限性

虽然 JSON 方法简单有效,但它有明显限制:

  • 无法处理函数、DateRegExpMapSet 等特殊对象;
  • 会忽略 undefined 和 Symbol 类型的属性;
  • 无法处理循环引用(会导致报错)。

✅ 更健壮的深拷贝方案可使用 structuredClone()(现代浏览器支持)或第三方库如 Lodash 的 _.cloneDeep()


三、栈内存 vs 堆内存:对比总结

特性栈内存(Stack)堆内存(Heap)
存储内容基本类型(numberstringboolean 等)引用类型(objectarrayfunction
分配方式自动、连续、固定大小动态、非连续、大小可变
访问速度快(直接寻址)相对较慢(需通过指针)
生命周期随函数调用入栈/出栈自动管理由垃圾回收机制(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 程序。