JS 递归深拷贝
· 阅读需 4 分钟
思路
从最基本的拷贝开始,一步一步处理更复杂的对象、类型。
拷贝对象字面量
先考虑基本类型和层层对象的情况,直接 for-in 遍历 + 递归拷贝
function deepClone(obj) {
// 基本类型直接返回
if (typeof obj !== 'object') return obj;
const newObj = {};
for (let key in obj) {
// 只复制自身的对象
if (Object.hasOwn(obj, key)) {
newObj[key] = deepClone(obj[key]);
}
}
return newObj;
}
解决循环引用问题
可以用 Map 或 WeakMap 解决,对复杂数据、对象字面量进行深拷贝前保存其引用,如果下次又出现了该值,就不再进行拷贝,直接返回保存的值。
function deepClone(obj, hash = new WeakMap()) {
if (typeof obj !== 'object') return obj;
if (hash.has(obj)) return hash.get(obj);
const newObj = {};
hash.set(obj, newObj);
for (let key in obj) {
if (Object.hasOwn(obj, key)) {
newObj[key] = deepClone(obj[key], hash);
}
}
return newObj;
}
现在已经支持复制这样的对象 o
了:
const o = {
name: 'Lee',
attrs: {
degree: 'BA',
year: 2019
}
};
const b = {};
o.reference = b;
b.reference = o;
处理构造出的且可直接赋值的对象
一些构造出来的对象,例如 Array 和 Object 的实例,可能是 [1,2,3]
或者 { a:1, b:2}
,他们支持 for-in 遍历,而且可以 obj[key] = value
这样赋值,就拿他们的构造函数 new 出一个新对象,然后放在 for-in 遍历里面拷贝每一个值即可。
function deepClone(obj, hash = new WeakMap()) {
if (typeof obj !== 'object') return obj;
if (hash.has(obj)) return hash.get(obj);
// 对象本身没 constructor 属性,但可以访问到,因为会沿着原型链往上找
// 如果是 {} ,就相当于 new Object();如果是数组,就相当于 new Array()
let newObj = new obj.constructor;
hash.set(obj, newObj);
for (let key in obj) {
if (Object.hasOwn(obj, key)) {
newObj[key] = deepClone(obj[key], hash);
}
}
return newObj;
}
处理其他常见类型
考虑 Date、RegExp、Set、Map、null 这些对象,需要用不同的手段来拷贝其值。
当对象是以下情况:
- null 对象直接返回 null
- Date 和 RegExp 对象,直接用构造函数包裹,new 出来返回即可
- Set 和 Map 集合,需要遍历每个值,并对每个值进行递归拷贝,最后返回新的集合
最终代码
function deepClone(obj, hash = new WeakMap()) {
if (obj === null) return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
if (obj instanceof Map) {
const o = new Map();
obj.forEach((val, key) => {
o.set(key, deepClone(val, hash));
});
return o;
}
if (obj instanceof Set) {
const s = new Set();
obj.forEach(v => {
s.add(v);
});
return s;
}
// 如果不 是对象,直接返回
if (typeof obj !== 'object') return obj;
if (hash.has(obj)) return hash.get(obj); // 3
// obj 是对象字面量或构造出的对象,比如 Array、Object 的实例 ([]和{})
let newObj = new obj.constructor;
// 将对象储存在weakMap中
hash.set(obj, newObj); // 1
// 递归地把原对象属性复制给创建的对象
for (let key in obj) {
if (Object.hasOwn(obj, key)) {
newObj[key] = deepClone(obj[key], hash); // 2
}
}
return newObj;
}
测试对象:
const obj = {
set: new Set([1, 2, 4]),
map: new Map([[1, '123', 2, '456']]),
arr: [1, 3, 5],
attr: {
a: 1, b: 2
},
date: new Date(),
reg: new RegExp(),
fun: function () {
console.log('fn');
}
};