在前端开发中,处理引用数据类型(对象、数组)时,深拷贝是一个绕不开的核心概念。它的本质是在内存的堆区重新开辟一块空间,完整地复制目标对象及其所有嵌套的子对象,从而彻底切断与原对象的引用联系。
为了在不同业务场景中做出最合理的选择,我们可以将前端实现深拷贝的方法从原生 API、历史 hack 方案到工程化实现进行系统梳理:
1. 现代原生标准:structuredClone()
这是目前浏览器原生提供的深拷贝全局方法,它基于结构化克隆算法(Structured Clone Algorithm),也是当下大部分常规业务的首选。
-
适用场景:现代浏览器环境(兼容性良好,Node.js 17+ 也已支持),需要拷贝包含内置对象(如
Map、Set、Date、RegExp、ArrayBuffer等)或存在循环引用的复杂数据。 -
优势:
-
原生 API,性能优异。
-
完美支持循环引用,不会导致栈溢出。
-
支持绝大多数 JavaScript 内置数据类型。
-
-
局限性:
-
无法拷贝函数(会抛出
DataCloneError异常)。 -
无法拷贝 DOM 节点。
-
无法保留对象的原型链(克隆出的对象不再是原类的实例,而是普通对象
Object)。
-
2. 经典的快捷方案:JSON.parse(JSON.stringify())
这是早期前端最常用的深拷贝方法,本质上是利用 JSON 的序列化和反序列化来实现内存的重新分配。
-
适用场景:纯粹的、不包含特殊类型和循环引用的简单数据载体(如后端返回的纯 JSON 树状结构)。
-
优势:代码极其简短,执行速度在处理纯数据时表现尚可,兼容性完美。
-
致命缺陷(边界情况多):
-
丢失数据:忽略
undefined、Symbol、函数。如果它们在数组中,会被转换为null;如果在对象中,该键值对直接消失。 -
类型畸变:
Date对象会被转换为字符串,RegExp和Error对象会被转换为空对象{},NaN、Infinity会被转为null。 -
报错:遇到循环引用会直接抛出
TypeError。
-
3. 第一性原理实现:手写递归拷贝
当现有 API 无法满足特殊的业务逻辑(例如必须拷贝函数,或者保留特定的原型链)时,我们需要从底层逻辑出发手写递归。其核心逻辑是:遍历目标对象的所有属性,遇到基本类型直接返回,遇到引用类型则递归调用自身,同时利用哈希表记录已拷贝的对象以打破循环。
JavaScript
function deepClone(target, map = new WeakMap()) {
// 1. 处理基础类型和 null
if (target === null || typeof target !== 'object') {
return target;
}
// 2. 处理循环引用
if (map.has(target)) {
return map.get(target);
}
// 3. 处理特定内置对象 (可以根据需要扩展 RegExp, Date 等)
if (target instanceof Date) return new Date(target);
if (target instanceof RegExp) return new RegExp(target);
// 4. 初始化克隆对象,保持原型链
const cloneTarget = Array.isArray(target) ? [] : Object.create(Object.getPrototypeOf(target));
// 5. 记录到 WeakMap 中
map.set(target, cloneTarget);
// 6. 递归处理属性 (包括 Symbol 属性)
Reflect.ownKeys(target).forEach(key => {
cloneTarget[key] = deepClone(target[key], map);
});
return cloneTarget;
}
- 核心细节:使用
WeakMap而不是Map来存储对象映射关系,是为了避免引起内存泄漏。WeakMap的键是弱引用,当原对象被垃圾回收时,其映射关系也会自动解除。
4. 稳健的工程化选择:第三方库(如 Lodash 的 _.cloneDeep)
在大型企业级项目中,如果你不想自己维护庞大且复杂的深拷贝工具函数,引入成熟的第三方库是降低风险的最佳方式。
-
适用场景:复杂的老旧项目,或者对各种边界情况(包括各种奇怪的类型对象、Buffer 等)有极高稳定要求的场景。
-
优势:久经沙场,几乎处理了 JavaScript 中所有能想到的边界情况。
-
局限性:会增加打包后的代码体积(虽然可以通过按需引入缓解),在处理简单对象时,由于内部判断逻辑过于庞大,性能开销反而比原生方法大。
方法对比总结
| 方案 | 循环引用支持 | 特殊对象支持 (Date, Map等) | 函数支持 | 原型链保留 | 性能/体积成本 |
|---|---|---|---|---|---|
structuredClone |
✅ | ✅ | ❌ | ❌ | 优 / 无负担 |
JSON 序列化 |
❌ | ❌ | ❌ | ❌ | 良 / 无负担 |
| 手写递归 | ✅ | 可定制 | 可定制 | 可定制 | 视实现复杂度而定 |
Lodash cloneDeep |
✅ | ✅ | ❌ | ❌ | 中 / 需引入库 |
注意:即便如 Lodash 这样强大的库,默认也是不深拷贝函数的,因为函数的执行上下文和闭包状态很难在运行时被完美复制,强行深拷贝函数在逻辑上往往是伪命题。
你想深入探讨一下在哪些极其特殊的业务场景下必须手写深拷贝,还是想了解一下这些不同方法在处理海量数据时的性能瓶颈差异?