🧬 JavaScript 深拷贝:如何彻底切断引用关联?
在 JavaScript 中,赋值操作对于引用类型 (Object, Array 等)只是复制了内存地址 (指针)。
这意味着两个变量指向堆内存中的同一个对象。
- 浅拷贝(Shallow Copy):只复制第一层属性。如果属性值是对象,则复制其引用。
- 深拷贝(Deep Copy) :递归复制所有层级。在堆内存中开辟一块全新的空间,存放完全独立的数据副本。
📂 目录
- [🤔 为什么需要深拷贝?](#🤔 为什么需要深拷贝?)
- [⚡ 方案一:JSON 序列化(最快但有限制)](#⚡ 方案一:JSON 序列化(最快但有限制))
- [🛠️ 方案二:结构化克隆 API
structuredClone(现代首选)](#🛠️ 方案二:结构化克隆 API structuredClone(现代首选)) - [💻 方案三:手写递归深拷贝(面试必考,最灵活)](#💻 方案三:手写递归深拷贝(面试必考,最灵活))
- [📚 方案四:第三方库 Lodash](#📚 方案四:第三方库 Lodash)
- [⚠️ 常见陷阱与注意事项](#⚠️ 常见陷阱与注意事项)
- [💡 总结](#💡 总结)
1. 🤔 为什么需要深拷贝?
先看一个经典的"事故现场":
javascript
const original = {
name: "Lingma",
info: {
age: 1,
skills: ["JS", "Vue"],
},
};
// ❌ 错误做法:直接赋值或浅拷贝
const copy = { ...original }; // 或者 Object.assign({}, original)
// 修改副本中的嵌套对象
copy.info.age = 99;
console.log(original.info.age); // 99 😱 原数据也被修改了!
原因 :... 展开运算符只复制了 original 的第一层。info属性是一个对象,复制的是它的引用地址 。copy.info 和 original.info 指向堆内存中的同一个对象。
深拷贝的目标 :让 copy 和 original 在内存中完全独立,互不影响。
2. ⚡ 方案一:JSON 序列化(最快但有限制)
这是最简单、最常用的"一行代码"深拷贝方法。
💻 代码实现
javascript
const original = { a: 1, b: { c: 2 } };
const copy = JSON.parse(JSON.stringify(original));
copy.b.c = 99;
console.log(original.b.c); // 2 ✅ 互不影响
✅ 优点
- 极简:无需引入库,无需手写逻辑。
- 快速:浏览器底层 C++ 优化,性能通常优于手写递归。
❌ 缺点(致命缺陷)
它会丢失 或转换以下数据类型:
undefined:会被忽略。Function:会被忽略。Symbol:会被忽略。Date:会变成字符串(ISO 格式),不再是 Date 对象。RegExp:会变成空对象{}。NaN,Infinity:会变成null。- 循环引用 :直接报错
TypeError: Converting circular structure to JSON。
适用场景:纯数据对象(POJO),不包含函数、日期、正则等特殊类型。
3. 🛠️ 方案二:结构化克隆 API structuredClone(现代首选)
ES2022 引入的全局 API,旨在解决 JSON 方法的局限性。
💻 代码实现
javascript
const original = {
date: new Date(),
map: new Map([["key", "value"]]),
set: new Set([1, 2]),
nested: { a: 1 },
};
const copy = structuredClone(original);
copy.nested.a = 99;
console.log(original.nested.a); // 1 ✅
console.log(copy.date instanceof Date); // true ✅ (JSON 方法做不到)
✅ 优点
- 原生支持:无需 polyfill(现代浏览器已支持)。
- 类型保留:支持 Date, Map, Set, RegExp, ArrayBuffer 等。
- 处理循环引用:不会报错,能正确处理环形结构。
❌ 缺点
- 不支持函数:依然无法拷贝 Function。
- 兼容性:旧版浏览器(如 IE)不支持,需检查环境。
适用场景:现代项目,需要拷贝包含 Date/Map/Set 但不含函数的复杂对象。
4. 💻 方案三:手写递归深拷贝(面试必考,最灵活)
在面试中,面试官通常会要求你手写一个深拷贝函数,以考察你对递归、类型判断和边界情况的处理。
🧠 核心思路
- 判断类型:如果是基本类型,直接返回。
- 处理特殊对象:Date, RegExp 等需特殊构造。
- 处理循环引用 :使用
WeakMap缓存已拷贝的对象,防止死循环。 - 递归拷贝:遍历对象/数组的每个属性,递归调用自身。
💻 代码实现
javascript
function deepClone(obj, hash = new WeakMap()) {
// 1. 处理 null 或非对象类型
if (obj === null || typeof obj !== "object") {
return obj;
}
// 2. 处理循环引用:如果已经拷贝过,直接返回缓存
if (hash.has(obj)) {
return hash.get(obj);
}
// 3. 处理特殊内置对象
if (obj instanceof Date) {
return new Date(obj.getTime());
}
if (obj instanceof RegExp) {
return new RegExp(obj.source, obj.flags);
}
// 如果需要支持 Map/Set,可在此添加逻辑
// if (obj instanceof Map) { ... }
// if (obj instanceof Set) { ... }
// 4. 创建新容器(保持原型链)
// 使用 Object.create 可以保留原型链,比 {} 或 [] 更严谨
const cloneObj = Array.isArray(obj)
? []
: Object.create(Object.getPrototypeOf(obj));
// 5. 存入缓存,防止循环引用
hash.set(obj, cloneObj);
// 6. 递归拷贝所有属性
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
🧪 测试循环引用
javascript
const a = { val: 1 };
a.self = a; // 循环引用
const b = deepClone(a);
console.log(b.val); // 1
console.log(b.self === b); // true ✅ 正确保持了内部引用关系
✅ 优点
- 可控:你可以决定如何处理函数、Symbol、DOM 节点等。
- 兼容性好:可在任何 JS 环境中运行。
- 面试加分项:展示了对 WeakMap、原型链和递归的深刻理解。
❌ 缺点
- 性能较差:递归开销大,深层嵌套对象可能栈溢出。
- 代码复杂:需要考虑很多边界情况。
5. 📚 方案四:第三方库 Lodash
在生产环境中,如果项目已经引入了 Lodash,直接使用 _.cloneDeep 是最稳妥的选择。
javascript
import _ from "lodash";
const copy = _.cloneDeep(original);
✅ 优点
- 健壮:经过多年社区验证,处理了绝大多数边缘情况。
- 省心:无需自己维护拷贝逻辑。
❌ 缺点
- 包体积 :如果只为这一个功能引入整个 Lodash,会增加打包体积(建议使用
lodash.clonedeep按需引入)。
6. ⚠️ 常见陷阱与注意事项
-
函数无法被深拷贝 :
函数是执行代码的逻辑,不是数据。通常深拷贝会忽略函数,或者你只能拷贝函数的字符串表示(
fn.toString()),但这失去了闭包上下文。 -
DOM 节点不能深拷贝 :
DOM 节点包含大量浏览器内部状态,不能用 JSON 或普通递归拷贝。如需拷贝 DOM,请使用原生 API
node.cloneNode(true)。 -
性能考量 :
深拷贝是非常昂贵的操作。对于大型数据(如几万条列表),频繁深拷贝会导致页面卡顿。
- 优化建议 :尽量使用不可变数据(Immutable Data)思维,只在必要时拷贝;或使用结构性共享(如 Immutable.js, Immer)。
-
Getter/Setter 丢失 :
普通的递归拷贝可能会丢失属性的
get/set描述符。如果需要保留,需使用Object.getOwnPropertyDescriptor和Object.defineProperty。
💡 总结
| 方案 | 优点 | 缺点 | 推荐指数 | 适用场景 |
|---|---|---|---|---|
| JSON | 简单、快 | 丢失函数/Date/undefined,不支持循环引用 | ⭐⭐⭐ | 纯数据对象,后端交互数据 |
| structuredClone | 原生、支持多种类型、支持循环引用 | 不支持函数,兼容性需注意 | ⭐⭐⭐⭐⭐ | 现代浏览器环境首选 |
| 手写递归 | 灵活、可定制、面试必备 | 性能一般,代码复杂 | ⭐⭐⭐⭐ | 面试、特殊定制需求 |
| Lodash | 稳健、功能全 | 增加包体积 | ⭐⭐⭐⭐ | 已使用 Lodash 的项目 |
🚀 博主寄语 :
深拷贝不是银弹。
在现代前端开发中,我们更推崇**不可变数据(Immutability)**的理念。
与其每次修改都深拷贝,不如使用 Immer 这样的库,通过 Proxy 代理来实现"看似可变,实则不可变"的高效状态管理。
记住选型原则:
- 简单数据用
JSON。- 现代环境用
structuredClone。- 复杂定制手写信。
- 生产环境靠 Lodash 或 Immer。
希望这篇文档能帮你彻底掌握深拷贝!如果有疑问,欢迎在评论区留言。👇
喜欢这篇文章吗?记得点赞、收藏、转发哦! ❤️