跳到主要内容

JS 递归深拷贝

· 阅读需 4 分钟
Hanasaki
阿巴阿巴阿巴

思路

从最基本的拷贝开始,一步一步处理更复杂的对象、类型。

拷贝对象字面量

先考虑基本类型和层层对象的情况,直接 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 这些对象,需要用不同的手段来拷贝其值。

当对象是以下情况:

  1. null 对象直接返回 null
  2. Date 和 RegExp 对象,直接用构造函数包裹,new 出来返回即可
  3. 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');
}
};