手写深拷贝(Deep Clone):优雅处理 RegExp、Date 和循环引用(Circular Ref)
大家好,欢迎来到今天的编程技术讲座。今天我们要深入探讨一个看似简单但实则非常复杂的主题——手写深拷贝(Deep Clone)。
你可能已经用过 JSON.parse(JSON.stringify(obj)) 来做深拷贝,但它有明显的局限性:无法处理 Date、RegExp、函数、undefined、Symbol 等类型,更别说循环引用了。而我们今天的目标是写出一个真正健壮、优雅且能处理边界情况的深拷贝函数。
一、为什么需要深拷贝?
在 JavaScript 中,对象和数组都是引用类型。如果你直接赋值:
const obj1 = { a: 1, b: [2, 3] };
const obj2 = obj1;
obj2.b.push(4);
console.log(obj1.b); // [2, 3, 4]
你会发现 obj1 也被改变了。这就是浅拷贝的问题。
深拷贝的核心目标是:创建一个新的对象或数组,其内部结构完全独立于原对象,修改新对象不会影响原对象。
二、常见深拷贝方法对比
| 方法 | 是否支持 Date | 是否支持 RegExp | 是否支持循环引用 | 性能 | 适用场景 |
|---|---|---|---|---|---|
JSON.parse(JSON.stringify(obj)) |
❌ | ❌ | ❌ | ⚡️快 | 简单数据结构(无特殊类型) |
Lodash 的 _.cloneDeep() |
✅ | ✅ | ✅ | 🐢慢(功能强大) | 生产环境推荐 |
| 自定义实现(本文重点) | ✅ | ✅ | ✅ | ⚡️中等 | 教学、理解原理、定制需求 |
💡结论:自定义深拷贝虽然复杂,但在理解底层机制和性能优化上有不可替代的价值。
三、核心挑战与解决方案设计
我们要解决三个关键问题:
1. 如何识别并正确复制特殊对象?
Date: 使用new Date(date.getTime())RegExp: 使用new RegExp(regexp.source, regexp.flags)Map/Set: 需要单独处理Function: 通常不拷贝(除非特别要求)
2. 如何避免无限递归?——循环引用检测
当对象 A 引用了 B,B 又间接或直接引用了 A,就会形成环。如果不加限制,递归会死循环。
解决方案:
- 使用 WeakMap 缓存已遍历的对象及其副本
- 每次遇到一个对象时,先查缓存,存在就返回缓存值,不存在再递归
3. 如何保证类型一致性?
- 数组 → 数组
- 对象 → 对象
- 原生对象(如 Date)→ 原生对象
- 不能把 Array 当成普通 Object 处理!
四、完整代码实现(带详细注释)
下面是我们的核心深拷贝函数:
function deepClone(obj, visited = new WeakMap()) {
// 1. 基本类型直接返回
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 2. 如果是已有对象,则返回缓存中的副本(防止循环引用)
if (visited.has(obj)) {
return visited.get(obj);
}
let cloned;
// 3. 特殊对象处理
if (obj instanceof Date) {
cloned = new Date(obj.getTime());
} else if (obj instanceof RegExp) {
cloned = new RegExp(obj.source, obj.flags);
} else if (obj instanceof Map) {
cloned = new Map();
obj.forEach((value, key) => {
cloned.set(key, deepClone(value, visited));
});
} else if (obj instanceof Set) {
cloned = new Set();
obj.forEach(value => {
cloned.add(deepClone(value, visited));
});
} else if (Array.isArray(obj)) {
// 4. 数组
cloned = obj.map(item => deepClone(item, visited));
} else {
// 5. 普通对象
cloned = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
cloned[key] = deepClone(obj[key], visited);
}
}
}
// 6. 缓存当前对象及其副本(用于后续循环引用检测)
visited.set(obj, cloned);
return cloned;
}
五、逐层解析关键逻辑
✅ 第一步:基础类型判断
if (obj === null || typeof obj !== 'object') {
return obj;
}
这是最基础的保护机制。如果传入的是数字、字符串、布尔值、null、undefined,直接返回即可。
✅ 第二步:循环引用检测(核心!)
if (visited.has(obj)) {
return visited.get(obj);
}
这里使用了 WeakMap 而不是普通 Map,因为:
- WeakMap 的键是弱引用,不会造成内存泄漏;
- 它适合用来缓存对象映射关系;
- 不会被垃圾回收器阻止(安全)。
✅ 第三步:特殊对象处理(优雅封装)
Date 类型
cloned = new Date(obj.getTime());
通过 .getTime() 获取时间戳再构造新 Date,是最稳妥的方式。
RegExp 类型
cloned = new RegExp(obj.source, obj.flags);
注意:source 是正则表达式的文本部分,flags 是修饰符(如 g, i, m),必须保留。
Map / Set 类型
这两个是 ES6 新增的数据结构,必须单独处理,否则会被当作普通对象处理(导致数据丢失)。
示例:
const map = new Map([['a', 1], ['b', 2]]);
const cloned = deepClone(map);
console.log(cloned.size); // 2
数组 vs 普通对象
cloned = obj.map(...); // 数组
// 或者
cloned = {}; // 普通对象
这确保了类型不变,比如 Array.isArray(obj) 必须保持为 true。
六、测试验证(含边界情况)
让我们用几个典型例子来测试我们的函数是否正确工作:
测试 1:基本对象 + 数组
const original = {
name: "Alice",
hobbies: ["reading", "coding"],
age: 25
};
const cloned = deepClone(original);
cloned.hobbies.push("travel");
console.log(original.hobbies); // ["reading", "coding"]
console.log(cloned.hobbies); // ["reading", "coding", "travel"]
✅ 正确分离!
测试 2:Date 和 RegExp
const obj = {
date: new Date("2023-01-01"),
regex: /hello/gi
};
const cloned = deepClone(obj);
cloned.date.setFullYear(2024);
cloned.regex.lastIndex = 10;
console.log(obj.date.getFullYear()); // 2023
console.log(cloned.date.getFullYear()); // 2024
console.log(obj.regex.lastIndex); // 0
console.log(cloned.regex.lastIndex); // 10
✅ 分离成功!
测试 3:循环引用(最关键!)
const a = { name: "A" };
const b = { name: "B", ref: a };
a.ref = b; // 形成环
const cloned = deepClone(a);
console.log(cloned.ref.ref.ref.name); // "A"
console.log(cloned.ref.ref === cloned); // false(不是同一个对象)
✅ 循环引用被正确处理,不会死循环!
测试 4:Map 和 Set
const map = new Map([['key', 'value']]);
const set = new Set([1, 2, 3]);
const clonedMap = deepClone(map);
const clonedSet = deepClone(set);
console.log(clonedMap.size); // 1
console.log(clonedSet.size); // 3
✅ 完美支持!
七、性能优化建议(进阶)
虽然上述实现已经很健壮,但在大规模数据下仍可进一步优化:
| 优化点 | 描述 | 实现方式 |
|---|---|---|
| 缓存策略 | 使用 WeakMap 已经很好,但可以考虑对某些高频类型预设缓存 | 如 Date.prototype.clone |
| 类型判断 | 减少重复调用 instanceof |
使用 Object.prototype.toString.call(obj) 更高效 |
| 递归深度限制 | 防止极端嵌套导致栈溢出 | 添加最大递归层数参数(如 maxDepth=100) |
例如改进版的类型判断:
function getType(obj) {
return Object.prototype.toString.call(obj).slice(8, -1);
}
// 使用:
if (getType(obj) === 'Date') { ... }
这样比多次 instanceof 更快,尤其在大量对象遍历时。
八、常见误区与陷阱提醒
| 误区 | 错误做法 | 正确做法 |
|---|---|---|
以为 for...in 就能遍历所有属性 |
忽略原型链上的属性 | 使用 Object.keys(obj) 或 Reflect.ownKeys(obj) |
| 忽略 Symbol 属性 | 默认跳过 Symbol | 显式处理:Reflect.ownKeys(obj) 包含 Symbol |
误以为 new Object() 和 {} 一样 |
实际上两者行为一致 | 无需担心,统一处理即可 |
| 深拷贝函数不传 visited 参数 | 导致循环引用报错 | 必须传递缓存容器,首次调用时初始化为空 |
🔍 提醒:不要试图用
JSON.stringify解决所有问题,它根本做不到!
九、总结:如何做到“优雅”的深拷贝?
我们最终达成的目标是:
✅ 功能完备:支持 Date、RegExp、Map、Set、数组、普通对象
✅ 安全性高:自动处理循环引用,避免栈溢出
✅ 性能合理:使用 WeakMap 缓存,减少冗余操作
✅ 易扩展:模块化设计,方便添加新的类型支持(如 TypedArray、Error 等)
这不是一次简单的编码练习,而是对 JS 内存模型、对象机制、递归控制和边界条件的全面考验。
正如一位资深工程师所说:“真正的深拷贝,不是写出来的,而是想明白后自然浮现的结果。”
十、附录:完整工具函数(可用于项目)
/**
* 深拷贝函数(支持 Date、RegExp、Map、Set、循环引用)
* @param {*} obj 待拷贝对象
* @returns {*} 深拷贝后的对象
*/
function deepClone(obj, visited = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (visited.has(obj)) return visited.get(obj);
let cloned;
if (obj instanceof Date) {
cloned = new Date(obj.getTime());
} else if (obj instanceof RegExp) {
cloned = new RegExp(obj.source, obj.flags);
} else if (obj instanceof Map) {
cloned = new Map();
obj.forEach((value, key) => cloned.set(key, deepClone(value, visited)));
} else if (obj instanceof Set) {
cloned = new Set();
obj.forEach(value => cloned.add(deepClone(value, visited)));
} else if (Array.isArray(obj)) {
cloned = obj.map(item => deepClone(item, visited));
} else {
cloned = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
cloned[key] = deepClone(obj[key], visited);
}
}
}
visited.set(obj, cloned);
return cloned;
}
你可以把这个函数直接放进你的工具库中,作为生产级使用的深拷贝方案。
希望今天的讲解让你不仅学会了怎么写深拷贝,更重要的是理解了背后的设计哲学:优雅 = 功能完整 + 安全可靠 + 易于维护。
下次你在面试或者工作中遇到这个问题时,不妨自信地说一句:“我写过一个能处理循环引用和特殊类型的深拷贝。” 😊
谢谢大家!