1、 JSON.parse()
与 JSON.stringify()
最简单的方式是使用 JSON.parse()
与 JSON.stringify()
进行深拷贝:
1
2
3
|
function cloneDeep(target) {
return JSON.parse(JSON.stringify(target));
}
|
但是它有一些缺陷:
- 无法处理对象内的循环引用
- 无法保持之前的原型链
- 无法处理
bigint
类型
- 无法正确处理 正则、Date 类型
- 会忽略
function
、undefined
类型
- 会忽略 key 为
symbol
类型的字段
2、MessageChannel
还可以使用 MessageChannel
来进行深拷贝:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
function deepClone(target) {
return new Promise((resolve) => {
const { port1, port2 } = new MessageChannel();
port2.onmessage = (ev) => resolve(ev.data);
port1.postMessage(target);
});
}
const obj = {
a: 1,
b: {
c: 1,
},
};
// 注意该方法是异步的
const cloneObj = await deepClone(obj);
|
上面的方法可以正确处理对象内的循环引用、bigint
、正则、Date 、undefined
类型;
但是下面的问题依然没有解决:
- 无法保持之前的原型链
- 会忽略 key 为
symbol
类型的字段
而且如果对象中有 function
,会直接报错。异步的方式用起来相对麻烦。
3、手动实现一个深拷贝方法
(1)、简单版本
1
2
3
4
5
6
7
8
9
10
11
12
|
function cloneDeep(target) {
if (target === null) return null;
if (typeof target !== "object") return target;
const newTarget = Array.isArray(target) ? [] : {};
for (let key in target) {
//不遍历其原型链上的属性
if (target.hasOwnProperty(key)) {
newTarget[key] = cloneDeep(target[key]);
}
}
return newTarget;
}
|
(2)、正确处理 正则、Date 类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
function cloneDeep(target) {
if (target === null) return null;
if (typeof target !== 'object') return target;
+ if (target.constructor === Date) return new Date(target);
+ if (target.constructor === RegExp) return new RegExp(target);
const newTarget = Array.isArray(target) ? [] : {};
for (let key in target) {
//不遍历其原型链上的属性
if (target.hasOwnProperty(key)) {
newTarget[key] = cloneDeep(target[key]);
}
}
return newTarget;
};
|
(3)、保持之前的原型链
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
function cloneDeep(target) {
if (target === null) return null;
if (typeof target !== 'object') return target;
if (target.constructor === Date) return new Date(target);
if (target.constructor === RegExp) return new RegExp(target);
+ const newTarget = new target.constructor(); // 保持原型链
- const newTarget = Array.isArray(target) ? [] : {};
for (let key in target) {
//不遍历其原型链上的属性
if (target.hasOwnProperty(key)) {
newTarget[key] = cloneDeep(target[key]);
}
}
return newTarget;
};
|
(4)、正确处理 key 为 symbol
类型的字段
for .. in ..
遍历不到 Symbol
类型的 key,其他的方式一般也遍历不到 Symbol
类型的 key ;
这里可以借助 Reflect.ownKeys
方法,他可以遍历当前对象的所有 key:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
function cloneDeep(target) {
if (target === null) return null;
if (typeof target !== 'object') return target;
if (target.constructor === Date) return new Date(target);
if (target.constructor === RegExp) return new RegExp(target);
const newTarget = new target.constructor(); // 保持原型链
- for (let key in target) {
- //不遍历其原型链上的属性
- if (target.hasOwnProperty(key)) {
- newTarget[key] = cloneDeep(target[key]);
- }
- }
+ Reflect.ownKeys(target).forEach(key => {
+ newTarget[key] = cloneDeep(target[key]);
+ })
return newTarget;
};
|
(5)、处理对象内的循环引用
处理循环引用的思路很简单,设置一个哈希表存储已拷贝过的对象,当检测到当前对象已存在于哈希表中时,取出该值并返回,不再做后续操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
+ function cloneDeep(target, map = new Map()) {
- function cloneDeep(target) {
if (target === null) return null;
if (typeof target !== 'object') return target;
if (target.constructor === Date) return new Date(target);
if (target.constructor === RegExp) return new RegExp(target);
+ if (map.has(target)) return map.get(target);
const newTarget = new target.constructor();
+ map.set(target, newTarget);
Reflect.ownKeys(target).forEach(key => {
+ newTarget[key] = cloneDeep(target[key], map);
- newTarget[key] = cloneDeep(target[key]);
})
return newTarget;
};
|
最终的深拷贝代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
function cloneDeep(target, map = new Map()) {
if (target === null) return null;
if (typeof target !== "object") return target;
if (target.constructor === Date) return new Date(target);
if (target.constructor === RegExp) return new RegExp(target);
if (map.has(target)) return map.get(target);
const newTarget = new target.constructor();
map.set(target, newTarget);
Reflect.ownKeys(target).forEach((key) => {
newTarget[key] = cloneDeep(target[key], map);
});
return newTarget;
}
|
它可以:
- 正确处理对象内的循环引用
- 保持之前的原型链
- 正确处理
bigint
类型
- 正确处理 正则、Date 类型
- 正确处理
function
、undefined
类型
- 正确处理 key 为
symbol
类型的字段
参考链接:
如何实现一个深拷贝
ECMAScript 6 入门:Reflect