JavaScript 深拷贝是前端开发里经常遇到的问题。对象和数组是引用类型,直接赋值只会复制引用,不会复制内部数据。修改新变量时,原对象也可能被影响,这在表单编辑、状态管理、缓存数据处理里很容易出问题。
深拷贝的目标,是创建一份新的数据结构,让新对象和原对象在嵌套层级上也互不影响。本文从浅拷贝和深拷贝区别讲起,再对比 JSON 方法、structuredClone 和递归实现方案。
浅拷贝问题
浅拷贝只复制第一层属性。如果属性值是对象或数组,复制过去的仍然是引用。
const user = {
name: 'Alice',
profile: { age: 20 }
};
const copy = { ...user };
copy.profile.age = 21;
console.log(user.profile.age);
上面代码里,profile 仍然指向同一个对象,所以修改副本的嵌套属性会影响原对象。
什么是深拷贝
深拷贝会递归复制对象内部的对象和数组,让每一层引用都变成新的。这样修改副本时,不会影响原始数据。
并不是所有场景都需要深拷贝。如果只是处理一层简单对象,浅拷贝就足够。深拷贝成本更高,也更容易遇到特殊类型和循环引用问题。

JSON 方法
最常见的简单深拷贝写法,是先把对象转成 JSON 字符串,再解析回来。
const copy = JSON.parse(JSON.stringify(obj));
这种方法简单粗暴,适合纯数据对象,比如只包含字符串、数字、布尔值、普通数组和普通对象的接口数据。
JSON 方法局限
JSON 方法有明显局限。它会丢失 undefined、函数、Symbol,无法正确处理 Date、Map、Set、正则等特殊类型,也无法处理循环引用。
const obj = {
date: new Date(),
fn: () => {},
value: undefined
};
如果数据来自后端接口,并且结构很干净,JSON 方法可以临时使用;如果数据里有复杂类型,就不适合。
structuredClone
structuredClone 是现代浏览器和运行环境提供的原生深拷贝方法。它能处理更多类型,比如 Date、Map、Set、ArrayBuffer,也能处理循环引用。
const copy = structuredClone(obj);
相比 JSON 方法,structuredClone 更可靠,也更语义化。如果项目运行环境支持它,普通深拷贝优先考虑这个方法。
structuredClone 限制
structuredClone 也不是万能的。它不能克隆函数、DOM 节点等不可结构化克隆的值。如果对象里有函数,会抛出错误。
因此在使用前要明确数据类型。如果你要复制的是状态数据、接口数据、配置对象,它通常很好用;如果对象里混合了方法、节点和复杂实例,就要谨慎。
递归实现思路
手写深拷贝的基本思路是:如果是基本类型,直接返回;如果是数组,创建新数组递归复制每一项;如果是对象,创建新对象递归复制每个属性。
function deepClone(value) {
if (value === null || typeof value !== 'object') {
return value;
}
if (Array.isArray(value)) {
return value.map((item) => deepClone(item));
}
const result = {};
for (const key in value) {
result[key] = deepClone(value[key]);
}
return result;
}
这个版本适合理解原理,但还不够完善。它没有处理循环引用,也没有处理 Date、Map、Set 等特殊类型。
处理循环引用
循环引用会让普通递归陷入死循环。可以使用 WeakMap 记录已经克隆过的对象。
function deepClone(value, cache = new WeakMap()) {
if (value === null || typeof value !== 'object') return value;
if (cache.has(value)) return cache.get(value);
const result = Array.isArray(value) ? [] : {};
cache.set(value, result);
for (const key in value) {
result[key] = deepClone(value[key], cache);
}
return result;
}
WeakMap 的 key 可以是对象,适合记录原对象和克隆对象之间的映射,避免循环引用导致无限递归。
特殊类型处理
如果要写更完整的深拷贝,还要处理 Date、正则、Map、Set 等类型。
if (value instanceof Date) {
return new Date(value.getTime());
}
if (value instanceof RegExp) {
return new RegExp(value.source, value.flags);
}
继续扩展当然可以,但代码会越来越复杂。实际项目里,如果没有特别需求,不建议自己维护一个过度复杂的深拷贝函数。
对象实例问题
类实例、带原型方法的对象,在深拷贝时也要格外注意。简单递归通常只复制普通属性,不一定保留原型链和方法行为。
如果数据本身是业务模型实例,最好不要随意深拷贝。更清晰的做法是提供明确的转换方法,或者只拷贝需要的纯数据部分。
性能注意事项
深拷贝会遍历整个数据结构,数据越大成本越高。在大型状态树、复杂列表、频繁交互中,盲目深拷贝可能带来性能问题。
如果只是更新对象的一小部分,可以考虑结构共享、局部浅拷贝,或者使用不可变数据更新工具。不要把深拷贝当成解决所有引用问题的万能方案。
怎么选择
如果是简单纯 JSON 数据,可以用 JSON 方法;如果运行环境支持,优先考虑 structuredClone;如果需要兼容旧环境或定制行为,可以写递归函数;如果对象类型复杂,建议使用成熟工具库或重新设计数据结构。
常见错误
第一种错误是以为展开运算符就是深拷贝。第二种错误是 JSON 方法用在包含 Date、函数或 undefined 的对象上。第三种错误是手写递归没有处理循环引用。第四种错误是在高频操作里反复深拷贝大对象。
实践建议
深拷贝前先判断是否真的需要。能用局部浅拷贝解决,就不要复制整棵数据树。确实需要深拷贝时,优先根据数据类型选择方案,而不是固定套某一种写法。
对大多数前端业务来说,深拷贝的重点不是背代码,而是理解引用关系、数据类型和性能成本。知道什么时候该拷贝、拷贝到什么程度,才是真正能避免 bug 的关键。














暂无评论内容