JavaScript 深拷贝学习笔记
在 JavaScript 中,数据类型分为基本数据类型(如 number、string、boolean 等)和引用数据类型(如 Object、Array、Function 等)。理解这两类数据在内存中的存储方式,是掌握深拷贝与浅拷贝的关键。
一、栈内存 vs 堆内存
- 基本数据类型存储在栈内存中,其特点是空间固定、访问高效。当我们执行
let d = a;这样的赋值操作时,实际上是将a的值复制一份给d,两者互不影响。 - 引用数据类型(如对象、数组)则存储在堆内存中,变量本身(如
users)在栈中仅保存一个指向堆内存的引用地址。因此,当执行const data = users;时,并没有创建新的对象,而是让data和users共享同一个堆内存地址。对data的修改会直接影响users,这就是浅拷贝的本质。
二、浅拷贝的问题
const users = [
{ Id: 1, Name: '张三', hometown: '北京' },
{ Id: 2, Name: '李四', hometown: '北京' }
];
const data = users; // 浅拷贝(引用赋值)
data[0].hobbies = ["篮球", "看烟花"];
console.log(users); // hobbies 已被修改!
上述代码中,data 与 users 指向同一块堆内存,任何一方的修改都会影响另一方。这在实际开发中极易引发难以追踪的 bug。
三、什么是深拷贝?
深拷贝是指:创建一个全新的对象,递归地复制原对象的所有层级属性,新旧对象在内存中完全独立,互不影响。
四、使用 JSON 方法实现深拷贝
一种简单且常用的深拷贝方法是利用 JSON.stringify() 和 JSON.parse():
const users = [
{ Id: 1, Name: '张三', hometown: '北京' },
{ Id: 2, Name: '李四', hometown: '北京' }
];
const data = JSON.parse(JSON.stringify(users)); // 深拷贝
data[0].hobbies = ["篮球", "看烟花"];
console.log(data); // 包含 hobbies
console.log(users); // 不受影响,无 hobbies
原理:
JSON.stringify(users)将对象序列化为字符串(此时脱离了引用关系);JSON.parse(...)将字符串反序列化为一个全新的对象,存入新的堆内存空间。
优点:
- 实现简单,一行代码即可完成;
- 适用于大多数纯数据对象(不含函数、undefined、Symbol 等)。
缺点:
- 无法处理函数、undefined、Symbol、Date、RegExp 等特殊类型;
- 无法处理循环引用(会导致报错);
- 会丢失原型链信息(拷贝后对象的 constructor 变为 Object)。
五、何时使用深拷贝?
- 需要对复杂对象进行“安全”的修改而不影响原始数据;
- 在状态管理(如 Redux)、表单编辑、数据快照等场景中非常常见;
- 传递数据给第三方库或 API 前,避免意外副作用。
六、其他深拷贝方案(补充)
对于更复杂的场景,可考虑:
- 使用 Lodash 的
_.cloneDeep(); - 手写递归深拷贝函数(需处理各种边界情况);
- 使用
structuredClone()(现代浏览器支持,ES2022 新增)。
总结
深拷贝是 JavaScript 开发中绕不开的重要概念。通过 JSON.stringify + JSON.parse 虽然不能覆盖所有情况,但在处理普通 JSON 数据时高效可靠。理解栈与堆内存的区别、引用赋值的本质,是写出健壮代码的基础。在实际项目中,应根据数据结构的复杂度选择合适的拷贝策略,避免因浅拷贝导致的数据污染问题。
记住:当你不确定是否需要深拷贝时,先问自己——“修改这个副本,会不会影响原始数据?” 如果答案是“不希望影响”,那就用深拷贝!