JSON.parse(JSON.stringify(obj))实现深拷贝的缺点

浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址;

深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存用于存放复制的对象,使这个增加的指针指向这个新的内存;

JSON.parse(JSON.stringify(obj))深拷贝的问题

  • 如果obj里面存在时间对象,JSON.parse(JSON.stringify(obj))之后,时间对象变成了字符串。
  • 如果obj里有RegExp、Error对象,则序列化的结果将只得到空对象。
  • 如果obj里有函数、undefined,则序列化的结果会把函数、undefined丢失。
  • 如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null。
  • JSON.stringify()只能序列化对象的可枚举的自有属性。如果obj中的对象是有构造函数生成的, 则使用JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的constructor。
  • 如果对象中存在循环引用的情况也无法正确实现深拷贝。
  • 不能处理循环引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
let person = {};
Object.defineProperties(person, {
'name': {
configurable: true,
enumerable: false,
writable: true,
value: 'test'
},
'age': {
configurable: true,
enumerable: true,
writable: true,
value: 10
}
});

let mySymbol = Symbol();
let obj = {
data0: 1,
data1: new Date(),
data2: new RegExp('\\W+'),
data3: new Error('1'),
data4: undefined,
data5: function () {
console.log(1);
},
data6: NaN,
data7: person,
data8: {
data_1: 3,
data_2: {
sub_data_1: 'heelo'
},
data_3: [1, 2, 3],
},
[mySymbol]: {
a: 'symbol'
}
}
console.log(obj);
console.log(JSON.parse(JSON.stringify(obj)));

为了解决上述JSON.parse(JSON.stringify(obj))实现深拷贝的缺点,可以通过递归实现一个深拷贝。

递归实现深拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function cloneDeep(target) {
if (typeof target === 'object' && target !== null) {
if (target.__proto__.constructor === Date) return new Date(target);
if (target.__proto__.constructor === RegExp) return new RegExp(target);
if (target.__proto__.constructor === Map) return new Map(target);
if (target.__proto__.constructor === Set) return new Set(target);
let res = new target.constructor();
for (let i of Reflect.ownKeys(target)) {
res[i] = cloneDeep(target[i]);
}
return res;
} else {
return target;
}
}

new target.constructor ()构造函数新建一个空的对象,而不是使用{}或者[],这样可以保持原形链的继承

测试结果:

循环引用

其实上面的递归实现深拷贝还存在一个问题:如果target.property = target,这样会形成了一个环(看着更像是套娃)。

那这个问题怎么解决呢?最开始的想法是如果存在这种循环引用的情况,可以找个地方先暂存一下。克隆之前,先去找是否存在一样(引用地址相同)的引用类型,如果有直接返回,没有再克隆。那么Map就很容易想到啦。

Map对象保存键值对。任何值(对象或者原始值) 都可以作为一个键或一个值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
let person = {};
Object.defineProperties(person, {
'name': {
configurable: true,
enumerable: false,
writable: true,
value: 'test'
},
'age': {
configurable: true,
enumerable: true,
writable: true,
value: 10
}
});

let mySymbol = Symbol();
let obj = {
data0: 1,
data1: new Date(),
data2: new RegExp('\\W+'),
data3: new Error('1'),
data4: undefined,
data5: function () {
console.log(1);
},
data6: NaN,
data7: person,
data8: {
data_1: 3,
data_2: {
sub_data_1: 'heelo',
sym: Symbol('symm')
},
data_3: [1, 2, 3],
},
[mySymbol]: {
a: 'symbol'
},
data10: Symbol('test'),
newMap: new Map([['age', 10]]),
newSet: new Set([1, 2, 3])
}

/*造成循环引用*/
obj.data9 = obj;

console.log(obj);
console.log(cloneDeep(obj));


function cloneDeep(target, map = new Map()) {
if (typeof target === 'object' && target !== null) {
let res = new target.constructor();

if (map.get(target)) {
return target;
}
map.set(target, res);

if (target.__proto__.constructor === Date) return new Date(target);
if (target.__proto__.constructor === RegExp) return new RegExp(target);
if (target.__proto__.constructor === Map) return new Map(target);
if (target.__proto__.constructor === Set) return new Set(target);

for (let i of Reflect.ownKeys(target)) {
res[i] = cloneDeep(target[i], map);
}
return res;
} else {
return target;
}
}

测试结果:

到这其实对日期对象(new Date())、正则对象(new RegExp())、MapSetSymbolfunctionundefinednullobjectArraynew Error()的拷贝。下面对代码结构优化一下,不希望出现那么多的if-else。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function cloneDeep(target, map = new Map()) {
if (typeof target === 'object' && target !== null) {
let res = new target.constructor();

if (map.get(target)) {
return target;
}
map.set(target, res);

let tagType = target.__proto__.constructor.name;

switch (tagType) {
case 'Date':
return new Date(target);
case 'RegExp':
return new RegExp(target);
case 'Map':
return new Map(target);
case 'Set':
return new Set(target);
default:
break;
}

for (let i of Reflect.ownKeys(target)) {
res[i] = cloneDeep(target[i], map);
}
return res;
} else {
return target;
}
}

参考文章