js 深拷贝和浅拷贝怎么实现
浅拷贝仅复制第一层属性、嵌套对象仍共享引用,可用 Object.assign 或展开运算符;深拷贝递归复制所有层级,优先用 structuredClone,兼容旧环境或克隆函数、类实例时用手写递归或 lodash.cloneDeep。
结论:浅拷贝只复制第一层属性,嵌套对象仍指向同一块内存,用 Object.assign 或展开运算符就能实现;深拷贝会递归复制所有层级,生产环境首选 structuredClone()(Chrome 98+/Node 17+),需要兼容旧浏览器或要克隆函数、自定义类实例时,再手写递归或上 lodash.cloneDeep。
浅拷贝和深拷贝到底有什么区别
区别只有一句话:拷贝后修改嵌套对象,原对象会不会跟着变。浅拷贝不会复制引用类型属性指向的对象,只复制引用地址;深拷贝会把每一层引用类型都新建一份。
const a = { name: 'x', info: { age: 1 } };
const shallow = { ...a };
shallow.info.age = 2;
console.log(a.info.age); // 2,原对象被改动
栈内存里存的是基本类型值和引用地址,堆内存里才是对象本体。浅拷贝只处理栈这一层,深拷贝要一直走到堆的末端。这是判断一个拷贝是深还是浅的唯一标准,跟用哪个 API 无关。
浅拷贝有哪几种写法
结论:Object.assign({}, obj)、{...obj}、[...arr]、arr.slice()、arr.concat() 都是浅拷贝,效果等价,日常用展开运算符最简洁。
三个必知的坑:
Object.assign和展开运算符只复制自有可枚举属性,原型链上的属性和不可枚举属性(比如Date的内部槽)会丢。{...new Date()}得到的是{},不是日期。Object.assign(target, source)会触发target上同名属性的 setter,展开运算符不会。所以Object.assign不能安全地用来合并带 setter 的类实例。- 两者都会复制 Symbol 类型的可枚举属性,这点比
for...in靠谱。
JSON.parse(JSON.stringify()) 为什么不能算通用深拷贝
结论:它只对「纯 JSON 数据」有效,遇到 undefined、函数、Symbol、Date、RegExp、Map、Set、循环引用中的任意一个都会出错或失真。
具体失真清单:undefined、函数、Symbol 属性被直接丢弃;NaN、Infinity 变成 null;Date 变成 ISO 字符串;RegExp 变成 {};Map/Set 变成 {};BigInt 直接抛 TypeError;循环引用抛 TypeError: Converting circular structure to JSON。性能上它还要先序列化再反序列化,大对象比递归慢。
能用的场景很窄:接口返回的纯数据对象,字段只有 string、number、boolean、null、数组和普通对象。
手写递归深拷贝怎么写
结论:递归实现必须解决三件事:处理循环引用、区分内置类型、保留原型。核心是用 WeakMap 记录已克隆对象,且必须在递归子属性之前写入。
function deepClone(target, map = new WeakMap()) {
if (target === null || typeof target !== 'object') return target;
if (map.has(target)) return map.get(target); // 循环引用
const type = Object.prototype.toString.call(target);
if (type === '[object Date]') return new Date(target);
if (type === '[object RegExp]') return new RegExp(target.source, target.flags);
let clone;
if (type === '[object Map]') {
clone = new Map(); map.set(target, clone);
target.forEach((v, k) => clone.set(deepClone(k, map), deepClone(v, map)));
return clone;
}
if (type === '[object Set]') {
clone = new Set(); map.set(target, clone);
target.forEach(v => clone.add(deepClone(v, map)));
return clone;
}
clone = Array.isArray(target) ? [] : Object.create(Object.getPrototypeOf(target));
map.set(target, clone); // 先登记,再递归
Reflect.ownKeys(target).forEach(k => {
clone[k] = deepClone(target[k], map);
});
return clone;
}
关键点:map.set(target, clone) 放在遍历子属性之前,否则 a.self = a 这种结构会栈溢出;用 Reflect.ownKeys 而不是 Object.keys,才能带上 Symbol 键和不可枚举键;Object.create(Object.getPrototypeOf(target)) 保住原型链,让 instanceof 依然成立。
structuredClone 能替代手写深拷贝吗
结论:能覆盖 90% 的业务场景,而且它由引擎原生实现,性能和正确性都优于手写版本,但有两类东西它拷不了。
支持范围:循环引用、Map、Set、Date、RegExp、ArrayBuffer、TypedArray、Blob、File、Error。
不支持范围:函数、Symbol、DOM 节点会直接抛 DataCloneError;自定义类的实例会被降级成普通对象,原型丢失;getter/setter 会被求值成普通属性,属性描述符不保留。
运行环境要求:Chrome 98、Firefox 94、Safari 15.4、Node.js 17 起原生可用。
实际项目里到底选哪个
结论:按数据形态选,不要一律上递归。
- 纯 JSON 数据、只在乎值:
structuredClone(),一行搞定。 - 目标环境可能低于 Node 17 或需要跑在旧版移动端 WebView:
lodash.cloneDeep,它内部处理了循环引用和绝大多数内置类型,是目前最稳的第三方方案。 - 对象里有函数、需要保留自定义类原型、需要精确控制哪些字段不拷贝:手写递归,并把
WeakMap和Reflect.ownKeys这两点写对。 - 需要保证不可变数据、又嫌深拷贝开销大:改用
immer这类结构共享方案,只重建被修改的路径,而不是整棵树。
记住一条判断线:只要被拷贝对象里存在引用类型的嵌套,就必须确认你用的是深拷贝;只写 {...obj} 却以为已经隔离,是线上数据被意外篡改最常见的来源。
转载请注明出处,版权归原作者所有。