在JavaScript开发中,我们经常需要复制一个对象或数组,但简单的赋值操作只是把引用地址传给了新变量。当原对象包含嵌套结构时,任何对副本的修改都会同步反映到原数据上。深拷贝的核心目标,是为每一层引用类型都创建全新的内存空间,使新旧数据彻底脱离关联。

为什么需要深拷贝
JavaScript中的数据类型分为基本类型(如number、string、boolean)和引用类型(如object、array、function)。基本类型在赋值时会直接复制值,而引用类型赋值只是复制了指向堆内存的指针。如果项目中多个模块共享同一个配置对象,其中一个模块修改了嵌套属性,其他模块读取到的数据就会意外变化,引发难以排查的bug。
浅拷贝工具例如Object.assign或展开运算符,只能复制第一层的属性。对于嵌套的对象,它们依然保留原来的引用。因此在处理来自接口的动态数据、表单状态快照、或者撤销重做功能的历史记录时,必须借助深拷贝来保证数据独立性。
使用JSON方法的局限
最容易被想到的深拷贝方案是借助JSON.stringify和JSON.parse。这种方法写法简单,在很多纯数据场景下确实可用。但它存在明显的盲区:函数、undefined、Symbol会被直接丢弃;Date对象会被转为字符串而非Date实例;RegExp、Map、Set等内置对象会丢失原型和方法;最致命的是,如果对象存在循环引用,JSON.stringify会直接抛出错误。
下面这段代码演示了JSON方案在处理特殊值时的表现:
const source = {
name: 'test',
fn: function() { return 1; }, // 函数会被忽略
time: new Date(), // 变成字符串
circle: null
};
source.circle = source; // 循环引用
try {
const copy = JSON.parse(JSON.stringify(source));
console.log(copy.fn); // undefined
console.log(copy.time); // 字符串,不是Date对象
} catch (e) {
console.log('报错:循环引用无法序列化');
}
从输出可以看出,JSON方案只适合处理“纯JSON数据”,也就是属性全是字符串、数字、布尔、数组和普通对象的场景。一旦数据结构复杂,就必须使用更完善的实现。
手写递归深拷贝函数
要覆盖更通用的场景,可以自己写一个递归函数。核心思路是:遇到基本类型直接返回;遇到引用类型,根据其构造函数类型创建对应的空容器,然后遍历键名递归拷贝每一项。为了处理循环引用,可以使用WeakMap记录已经拷贝过的对象,当再次遇到同一引用时直接返回缓存的副本。
下面的实现兼顾了数组、普通对象、Date和RegExp,并且通过WeakMap避免了循环引用导致的栈溢出:
function deepClone(target, cache = new WeakMap()) {
// 基本类型或函数直接返回
if (target === null || typeof target !== 'object') {
return target;
}
// 处理循环引用
if (cache.has(target)) {
return cache.get(target);
}
// 处理Date和RegExp
if (target instanceof Date) {
return new Date(target);
}
if (target instanceof RegExp) {
return new RegExp(target.source, target.flags);
}
// 创建对应类型的容器
const clone = Array.isArray(target) ? [] : {};
cache.set(target, clone);
// 递归拷贝可枚举自有属性
Reflect.ownKeys(target).forEach(function(key) {
clone[key] = deepClone(target[key], cache);
});
return clone;
}
// 测试
const obj = { a: 1, b: { c: 2 }, d: new Date() };
obj.self = obj;
const result = deepClone(obj);
console.log(result.b === obj.b); // false,已实现深拷贝
console.log(result.self === result); // true,循环引用被正确处理
这个函数在大多数业务代码中已经足够健壮。它使用Reflect.ownKeys拿到包括Symbol在内的所有键,保证特殊键名也不会遗漏。WeakMap的键是弱引用,不会造成内存泄漏,适合在拷贝过程中做临时缓存。
其他注意事项与优化
上述手写方案仍然没有处理Map、Set、ArrayBuffer等更多内置类型,也没有拷贝对象原型链上的方法。如果你面对的是极其复杂的数据结构,可以考虑使用社区成熟库如lodash的cloneDeep,它内部对几十种类型做了分支判断。另外,如果数据层级非常深且体积巨大,递归可能导致调用栈过长,此时可改为循环加栈结构的非递归写法。
在性能方面,深拷贝本身是一项开销较大的操作。实际开发中应避免在渲染循环里频繁深拷贝大对象,可以通过结构共享(如Immutable.js的思路)或只拷贝发生变化的部分来降低代价。理解深拷贝的原理和边界,才能在不同场景下选择最合适的复制策略。
JavaScript深拷贝递归修改时间:2026-08-07 05:36:22