前言
无论是面试中还是实际工作中,遇到深浅拷贝的概率都挺大的。
由于浅拷贝比较简单,本文中就不提了,主要聊一聊深拷贝。
首先列举出深拷贝常见的几种方式:
- 迭代法
- 序列化和反序列化
Object.create()
1. 简单对象的深拷贝
对于数据类型简单的对象,如:
const obj = {
a: {
b: 1,
c: 2
},
d: [3, 4]
}
1.1 for in实现
我们用简单的for in递归就可以实现此对象的深拷贝
function deepClone(payload) {
// payload 不是对象则不执行
if (typeof payload !== 'object') return;
const result = Array.isArray(payload) ? [] : {};
for (let key in payload) {
result[key] = typeof payload[key] === 'object' ? deepClone(payload[key]) : payload[key];
}
return result;
}
let obj2 = deepClone(obj);
obj.a.b = 2;
obj.d.push(10);
console.log(obj2.a.b); // 1
console.log(obj2.d); // [3, 4]
这样 obj2 就是拷贝出的一个新对象而不受 obj 中值发生改变的影响。
1.2 Reflect实现
function deepClone2(obj) {
// obj 不是对象则不执行
if (typeof obj !== 'object') return;
let res = Array.isArray(obj) ? [...obj] : { ...obj }
Reflect.ownKeys(res).forEach( key => {
res[key] = typeof obj[key] === 'object' ? deepClone2(obj[key]) : obj[key];
})
return res;
}
let obj2 = deepClone2(obj);
obj.a.b = 2;
obj.d.push(10);
console.log(obj2.a.b); // 1
console.log(obj2.d); // [3, 4]
这个方法使用了Reflect.ownKeys方法遍历后递归拷贝,我们发现结果和for in的方法得到的结果一样,那两者的区别是什么我们后面再说。
1.3 JSON.parse实现
function deepClone3(payload) {
return JSON.parse(JSON.stringify(payload))
}
使用这种方法的问题在于以下几点
- 会忽略
undefined - 会忽略
symbol - 不能序列化函数
- 不能解决循环引用的对象
- 会抛弃对象的
constructor
也就是这种方式的深拷贝,只能处理那些能用json表示的对象,而且拷贝后,不管这个对象原来的构造函数是什么,都会变成Object类型。
以上就是对于简单对象深拷贝的实现,上述三种方法可以满足一些简单场景
2. 复杂对象的深拷贝
对于对象中只存在数组、Object类型的数据,可以使用上述简单的方式处理,但是如果对象属性中有方法或者其他类型的对象,应该考虑更多边界情况
// 实现如下对象的深拷贝
{
a: {
name: '我是一个对象',
id: 1
},
arr: [0, 1, 2],
fn: () => {
console.log('我是一个函数')
},
date: new Date(),
reg: new RegExp('/我是一个正则/ig'),
err: new Error('我是一个错误'),
[Symbol()]: 'symbol'
}
对于这样的一个对象,如果运用上面的三种方法,我们发现用前两种方法,得到的结果分别是这样的
// for in 方法
{
a: { name: '我是一个对象', id: 1 },
arr: [ 0, 1, 2 ],
fn: [Function: fn],
date: {},
reg: {},
err: {}
}
// Reflect
{
a: { name: '我是一个对象', id: 1 },
arr: [ 0, 1, 2 ],
fn: [Function: fn],
date: {},
reg: { lastIndex: 0 },
err: {
stack: 'Error: 我是一个错误...',
message: '我是一个错误'
},
[Symbol()]: 'symbol'
}
可以看到两者的区别在于Reflect可以遍历到Symbol,并且对Regex和Error有值拷贝效果。而用序列化的方法拷贝的话,结果是数组和Object以外的其他对象都会失真,date只剩一个字符串了。
因此需要基于
Reflect并处理各边界条件。
2.1 解决function类型
首先对于function类型的值,我们可以先把它转换为字符串,再使用eval方法。
改进后的代码:
function deepClone3(payload) {
if (typeof payload === 'function') return eval(payload.toString());
// obj 不是对象则直接返回
if (typeof payload !== 'object') return payload;
let res = Array.isArray(payload) ? [] : {};
Reflect.ownKeys(payload).forEach( key => {
res[key] = deepClone3(payload[key]);
})
return res;
}
2.2 解决各构造函数
根据原数据可能出现的数据结构有: Boolean、Date、Number、String、RegExp及其他自定义类。
对于这些数据采用获取构造函数,在进行实例化的方式实现。
const constructor = payload.constructor;
switch(constructor) {
case String:
case Number:
case Boolean:
case Date:
case RegExp: return new constructor(payload);
default: res = new constructor();
}
至此,对于本章开始提到的数据可以实现完整的深拷贝过程。但是,当数据中出现循环引用的值,如obj.o = obj,还需要做最后一步处理。
2.3 解决循环引用的问题
针对循环引用的情况,递归过程容易出现栈溢出,因此采用WeakMap数据结构存入key,当WeakMap内已存在当前key则直接返回数据,避免陷入死循环。
完整实现深拷贝的代码如下:
function deepClone3(payload, hash = new WeakMap()) {
console.log(payload);
// 解决循环引用问题
if (hash.has(payload)) return hash.get(payload);
// 解决function类型
if (typeof payload === 'function') return eval(payload.toString());
// 基本数据类型直接返回
if (typeof payload !== 'object') return payload;
let res;
const constructor = payload.constructor;
switch(constructor) {
case String:
case Number:
case Boolean:
case Date:
case RegExp: return new constructor(payload);
default:
res = new constructor();
// 存入weakMap
hash.set(payload, res);
}
Reflect.ownKeys(payload).forEach( key => {
res[key] = deepClone3(payload[key], hash);
})
return res;
}
3. 总结
本文根据现有数据类型,基于Reflect增加各情况判断,基本完整的实现了深拷贝函数。可能还有些没有考虑到的情况,如果小伙伴们有其他测试用例会导致此函数报错的,欢迎及时指正。