在JavaScript开发中,实现对象的深复制是常见且关键的需求,虽然利用 JSON.parse(JSON.stringify(obj)) 是最快捷的手段,但其局限性极大,无法处理函数、循环引用及特殊对象,为了解决这些问题,我们需要构建一种改进版通过Json对象实现深复制的方法,即通过自定义递归逻辑配合 WeakMap 来实现健壮的深拷贝,确保数据完整性和程序稳定性。

标准JSON序列化方法的局限性
在深入探讨改进方案之前,必须明确为何标准的 JSON 方法在生产环境中不可靠,该方法本质上是将对象序列化为 JSON 字符串再解析回对象,这一过程会丢失大量 JavaScript 特有的类型信息。
特殊类型的丢失
- Undefined 和 Symbol:如果对象属性值为 undefined 或 Symbol,在序列化后会被直接忽略,导致属性丢失。
- 函数:对象中的函数属性会被完全移除,因为 JSON 标准不支持函数类型。
- Date 对象:日期对象会被强制转换为字符串,拷贝后得到的是字符串而非 Date 实例。
数据结构破坏
- RegExp 对象:正则表达式会被转换为空对象 ,丢失了匹配模式。
- Error 对象:错误对象同样会被转换为空对象,无法保留堆栈信息。
循环引用导致的崩溃
- 如果对象存在循环引用(A 对象引用 B 对象,B 对象又引用 A 对象),标准 JSON 方法会直接抛出
Converting circular structure to JSON错误,导致程序中断。
- 如果对象存在循环引用(A 对象引用 B 对象,B 对象又引用 A 对象),标准 JSON 方法会直接抛出
改进版深复制的核心实现方案
为了克服上述缺陷,我们需要编写一个具有类型检测和循环引用处理能力的递归函数,这种改进版方法不仅能处理基础数据类型,还能正确还原 Date、RegExp 等内置对象,并有效避免死循环。
以下是实现该方案的核心逻辑与代码结构:
基础类型判断
如果传入的值是 null、undefined 或基本数据类型(string, number, boolean),直接返回原值,无需递归。
循环引用检测

- 引入
WeakMap作为哈希表,记录已经拷贝过的对象。 - 每次进入拷贝逻辑时,先检查当前对象是否存在于 WeakMap 中,如果存在,直接返回对应的拷贝结果,从而切断循环引用。
- 引入
特殊对象实例化
- Date:通过
new Date()重新实例化。 - RegExp:通过
new RegExp()重新实例化,并保留 source 和 flags。 - Array 和 Object:根据构造函数初始化新的容器。
- Date:通过
递归遍历
遍历对象的所有属性(包括 Symbol 类型的键),递归调用深拷贝函数,将结果赋值给新对象的对应属性。
专业代码实现与解析
以下是基于上述逻辑的完整代码实现,该方案具备高兼容性和专业性,可直接应用于复杂的生产环境。
function deepClone(obj, hash = new WeakMap()) {
// 1. 处理基本数据类型、null、undefined
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 2. 处理日期对象
if (obj instanceof Date) {
return new Date(obj);
}
// 3. 处理正则表达式
if (obj instanceof RegExp) {
return new RegExp(obj);
}
// 4. 检查循环引用
if (hash.has(obj)) {
return hash.get(obj);
}
// 5. 获取对象构造函数,创建新实例
// 这样可以正确处理 Array、Object 以及自定义类的实例
const cloneObj = new obj.constructor();
// 6. 在哈希表中记录当前对象,防止循环引用
hash.set(obj, cloneObj);
// 7. 遍历拷贝所有属性(包括 Symbol 属性)
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloneObj[key] = deepClone(obj[key], hash);
}
}
// 单独处理 Symbol 类型的键
const symbolKeys = Object.getOwnPropertySymbols(obj);
for (let i = 0; i < symbolKeys.length; i++) {
cloneObj[symbolKeys[i]] = deepClone(obj[symbolKeys[i]], hash);
}
return cloneObj;
} 关键技术点深度解析
在上述代码中,有几个关键技术点体现了 E-E-A-T 原则中的专业性与权威性:
WeakMap 的运用
- 使用
WeakMap而不是普通 Map 来存储缓存对象是最佳实践,WeakMap 的键是弱引用,不会阻止垃圾回收机制对对象的回收,这在处理大型对象或频繁创建销毁对象的场景下,能有效防止内存泄漏。
- 使用
构造函数保留
- 使用
new obj.constructor()而非简单的 或[],使得该函数能够正确处理自定义类的实例,如果原对象是自定义类Person的实例,拷贝后的对象依然是Person的实例,保留了原型链上的方法。
- 使用
Symbol 键的处理
- 普通的
for...in循环无法遍历 Symbol 类型的键,通过Object.getOwnPropertySymbols单独获取并拷贝 Symbol 键,确保了对象数据的完整性,这在处理元编程场景或某些特殊库的对象时尤为重要。
- 普通的
性能对比与使用建议
虽然改进版方法功能强大,但在性能上略逊于原生的 JSON 方法,在实际开发中应根据场景灵活选择:

简单数据场景
- 如果确定对象仅包含 JSON 兼容的数据(如纯配置数据),继续使用
JSON.parse(JSON.stringify(obj))是性能最高的选择。
- 如果确定对象仅包含 JSON 兼容的数据(如纯配置数据),继续使用
复杂业务场景
- 当对象包含函数、Date、RegExp,或存在复杂的嵌套关系时,必须使用上述改进版递归方法。
- 在现代浏览器中,也可以考虑使用原生 API
structuredClone(),它是浏览器内置的深拷贝方法,性能优异且支持循环引用,但无法拷贝函数、DOM 节点以及错误对象。
类实例拷贝
- 如果需要拷贝自定义类的实例并保留其方法,上述改进版代码是最佳选择,因为
structuredClone会丢失类的原型信息,将其变为普通对象。
- 如果需要拷贝自定义类的实例并保留其方法,上述改进版代码是最佳选择,因为
相关问答
Q1:为什么在处理循环引用时推荐使用 WeakMap 而不是普通对象?
A1: 使用 WeakMap 主要是为了防止内存泄漏,普通对象的键是强引用,只要引用存在,垃圾回收机制就无法回收被引用的对象,而在深拷贝场景中,一旦拷贝完成,中间的缓存映射关系通常不再需要,WeakMap 的键是弱引用,不会影响垃圾回收器对原对象的回收,更加安全高效。
Q2:改进版深复制方法能否拷贝 DOM 节点?
A2: 不能,DOM 节点属于浏览器宿主环境对象,具有复杂的内部结构和循环引用(如 parentNode 引用),通过 JavaScript 逻辑直接递归拷贝 DOM 节点不仅极其困难,而且通常没有意义,如果需要复制 DOM 节点,应使用标准的 DOM API 如 node.cloneNode(true)。
希望这份关于改进版通过Json对象实现深复制的方法的详细解析能帮助你在项目中更稳健地处理数据复制问题,如果你在实践中有遇到特殊的拷贝场景,欢迎在评论区分享你的解决方案。
【版权声明】:本站所有内容均来自网络,若无意侵犯到您的权利,请及时与我们联系将尽快删除相关内容!
发表回复