深拷贝
深拷贝(Deep Clone)指:复制出一个全新的数据结构,使得修改新对象的任意层级,不会影响原对象。
- 浅拷贝:只拷贝一层引用(例如
{...obj}、arr.slice()),嵌套对象仍共享引用。 - 深拷贝:递归地拷贝所有可达子对象(并处理循环引用)。
什么时候需要深拷贝
- 需要在不影响原数据的情况下做“试算/回滚”(例如表单草稿、撤销重做)
- Redux/不可变数据没用到结构共享,需要简单“隔离”数据
- 对象结构复杂且会被多处修改(避免意外的引用共享)
如果你的数据天然是“不可变”的(只读 + 通过新对象更新),通常不需要深拷贝。
常见方案对比
JSON.parse(JSON.stringify(x)):简单,但丢信息多,且不支持循环引用。structuredClone(x):现代内置方案,支持循环引用与很多内置类型,但不能克隆函数/DOM 节点等。- 递归拷贝:可控、可扩展;需要自己处理类型与循环引用。
1) 最简方案:JSON 深拷贝(不推荐用于通用场景)
优点:写法短。
局限(面试高频):
function、symbol会丢失undefined会在对象属性里丢失(数组中会变成null)Date/RegExp/Map/Set/Error等类型会被“拍扁”成普通对象/字符串- 遇到循环引用直接报错
const source = {
b: { name: 'xht' },
foo: () => 5,
3: undefined
};
const deep = JSON.parse(JSON.stringify(source));
// deep => { b: { name: 'xht' } }2) 现代内置:structuredClone(优先推荐)
浏览器与 Node.js 新版本提供 structuredClone,特点:
- 支持循环引用
- 支持多种内置类型(如
Date、Map、Set、ArrayBuffer等) - 不支持函数(会抛错)
// 运行环境支持时直接用
const copy = structuredClone({ a: 1, b: new Date() });3) 递归版(无循环引用时可用)
优点:好理解。 缺点:不处理循环引用;对特殊类型支持弱。
export function baseClone<T>(input: T): T {
if (typeof input !== 'object' || input === null) return input;
const result: any = Array.isArray(input) ? [] : {};
for (const key in input as any) {
result[key] = baseClone((input as any)[key]);
}
return result;
}4) 通用版:WeakMap 处理循环引用(推荐自实现方式)
为什么用 WeakMap
用 Map 也能实现“缓存已克隆对象”,但 Map 会强引用 key,可能造成缓存长期存在时的内存压力。
WeakMap 的 key 是弱引用:
- 不阻止垃圾回收
- 更不容易引入内存泄漏
一个更稳的 cloneDeep
特性:
- 支持循环引用
- 支持
Date、RegExp、Map、Set - 保留原型(
Object.create(proto)) - 复制自有属性(包含
symbolkey),尽量保留属性描述符(getter/setter 不会被“执行”)
export function cloneDeep<T>(input: T, cache = new WeakMap<object, any>()): T {
// 基础类型、函数:直接返回
if (typeof input !== 'object' || input === null) return input;
if (typeof input === 'function') return input;
const target = input as unknown as object;
if (cache.has(target)) return cache.get(target);
// 内置类型
if (input instanceof Date) {
const result = new Date(input.getTime());
cache.set(target, result);
return result as unknown as T;
}
if (input instanceof RegExp) {
const result = new RegExp(input.source, input.flags);
cache.set(target, result);
return result as unknown as T;
}
if (input instanceof Map) {
const result = new Map();
cache.set(target, result);
input.forEach((value, key) => {
result.set(cloneDeep(key as any, cache), cloneDeep(value as any, cache));
});
return result as unknown as T;
}
if (input instanceof Set) {
const result = new Set();
cache.set(target, result);
input.forEach((value) => {
result.add(cloneDeep(value as any, cache));
});
return result as unknown as T;
}
// Array
if (Array.isArray(input)) {
const result: any[] = new Array(input.length);
cache.set(target, result);
for (let i = 0; i < input.length; i++) {
result[i] = cloneDeep((input as any)[i], cache);
}
return result as unknown as T;
}
// 普通对象 / 类实例:保留原型 + 复制自有属性
const proto = Object.getPrototypeOf(input);
const result = Object.create(proto);
cache.set(target, result);
// Reflect.ownKeys取出自有key,不包括原型
/**
* 目的:拿到 所有自有属性键,包括:
字符串 key(含不可枚举的)、symbol key
为什么不用 for...in / Object.keys:
for...in 会枚举 原型链 上的属性(不想要)
Object.keys 只拿 可枚举的字符串 key(会漏掉不可枚举、会漏掉 symbol
*/
Reflect.ownKeys(input as any).forEach((key) => {
/**
* 目的:拿到属性的 描述符 desc,里面包含:
数据属性:{ value, writable, enumerable, configurable }
访问器属性:{ get, set, enumerable, configurable }
这么做的意义:
你可以原样复制 enumerable/configurable/writable
遇到 getter/setter 时,不会通过 input[key] 去“读取值”从而触发 getter(避免副作用)
*/
const desc = Object.getOwnPropertyDescriptor(input as any, key);// 取出这个key的属性描述符
if (!desc) return;
if ('value' in desc) {
desc.value = cloneDeep((input as any)[key as any], cache);
}
Object.defineProperty(result, key, desc); // 替换 obj中的key新的描述符
});
return result;
}示例:循环引用
const source2: any = {
3: undefined,
foo: () => 8,
data: new Date(),
tags: new Set(['a', 'b']),
dict: new Map([['k', { v: 1 }]])
};
source2.self = source2;
const copied = cloneDeep(source2);
copied === source2; // false
copied.self === copied; // true(循环结构被保留)复杂度
设对象图中可达节点总数为 (属性/元素/Map-Set entries 总量):
- 时间:
- 空间:(包含结果与
WeakMap缓存)
常见坑点
- 函数怎么处理:多数深拷贝实现会“按引用返回函数”,因为函数闭包环境无法被复制。
- DOM 节点:不建议深拷贝 DOM;应该重新查询/重新创建。
- 类实例:如果需要保留原型/方法,至少要
Object.create(proto),否则会退化为普通对象。 - 属性描述符 / getter-setter:简单的
result[key] = ...会触发 getter;如需严谨可复制 descriptor。