JS05 深拷贝

实现一个深拷贝。

乞丐版

可以使用JSON.parse(JSON.stringify())来实现最简单的深拷贝,但是有着很多的缺陷,比如拷贝引用类型、拷贝函数、循环引用等

基础版

使用递归来完成深层对象的深拷贝,考虑数组的区别:

1
2
3
4
5
6
7
8
9
10
11
function deepClone(target) {
if (typeof target !== 'object') {
return target;
}

const clonedTarget = Array.isArray(target) ? [] : {};
for (const key in target) {
clonedTarget[key] = deepClone(target[key]);
}
return clonedTarget;
}

进阶版

上面的代码执行下面的测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
const target = {
field1: 1,
field2: undefined,
field3: {
child: 'child'
},
field4: [2, 4, 8]
};

target.target = target;

console.log(deepClone(target));

控制台会报错,因为递归进入了死循环,导致内存溢出了,所以基础版需要解决循环引用的问题

解决方法就是用一个临时数组,数组成员是一个对象,target属性存放被拷贝的原始对象,clonedTraget存放已经递归过的对象,在递归一个对象之前,检查这个数组中是否存在,如果存在就直接返回,不再对其进行递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function deepClone(target, cache = []) {
if (typeof target !== 'object') {
return target;
}

const cachedTarget = cache.find((v) => v.target === target);
if (cachedTarget) {
return cachedTarget.clonedTarget;
}

const clonedTarget = Array.isArray(target) ? [] : {};
cache.push({clonedTarget, target});

for (const key in target) {
clonedTarget[key] = deepClone(target[key], cache);
}
return clonedTarget;
}

进阶优化版1

如果使用ES6的Map对象,可以对上面的cache做一些优化,普通的JavaScript对象不能用引用对象作为key值,而Map对象可以,这样我们就可以直接使用被拷贝的原始对象作为key值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function deepClone(target, cache = new Map()) {
if (typeof target !== 'object') {
return target;
}

const cachedTarget = cache.get(target);
if (cachedTarget) {
return cachedTarget;
}

const clonedTarget = Array.isArray(target) ? [] : {};
cache.set(target, clonedTarget);

for (const key in target) {
clonedTarget[key] = deepClone(target[key], cache);
}
return clonedTarget;
}

进阶优化版2

上面的代码中,使用了Map对象,如果被拷贝的对象很大、层次很深的时候,cache会占用大量的内存,这个时候可以考虑使用WeakMap来进行性能上的优化

WeakMap的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。

举个例子,如果使用Map的话,对象间是存在强引用关系的

1
2
3
4
let obj = { name : 'ConardLi'}
const map = new Map();
map.set(obj,'code秘密花园');
obj = null;

虽然手动删除了obj,但是target依然对obj存在强引用关系,这部分内存仍然无法被释放

如果使用WeakMap

1
2
3
4
let obj = { name : 'ConardLi'}
const wm = new WeakMap();
wm.set(obj,'code秘密花园');
obj = null;

wmobj是弱引用关系,当下次垃圾回收机制执行时,不会考虑wmobj的引用,这块内存会被清除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function deepClone(target, cache = new WeakMap()) {
if (typeof target !== 'object') {
return target;
}

const cachedTarget = cache.get(target);
if (cachedTarget) {
return cachedTarget;
}

const clonedTarget = Array.isArray(target) ? [] : {};
cache.set(target, clonedTarget);

for (const key in target) {
clonedTarget[key] = deepClone(target[key], cache);
}
return clonedTarget;
}

完善数据类型

引用类型的判断

非引用类型直接返回,不需要递归,上面的代码只是使用了typeof来判断,但是对于nullfunction,需要作特殊处理

1
2
3
function isObject(target) {
return target !== null && (typeof target === 'object' || typeof target === 'function');
}

获取准确的数据类型

使用Object.prototype.toString.call来判断数据的准确类型,返回结果是'[object type]'

1
2
3
function getType(target) {
return Object.prototype.toString.call(target);
}

1
2
3
4
5
6
7
8
9
10
11
12
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';

const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const regexpTag = '[object RegExp]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';

上面的类型中可以分为可以继续遍历的类型和不可继续遍历的类型

不可以继续继续遍历的类型

对了对象、数组、SetMap之外,上面列出的其他类型都属于不可以继续继续遍历的类型,不再需要递归,直接返回一个值就可以,具体实现根据不同的类型有不同的处理方法

1
2
3
4
5
6
7
8
9
10
11
12
13
function cloneOtherType(target, type) {
switch (type) {
case regexpTag: {
return cloneReg(target);
}
case symbolTag: {
return cloneSymbol(target);
}
default: {
return new target.constructor(target)
}
}
}

通过构造函数复制

BooleanDateErrorStringNumber等类型都可以直接使用构造函数和原始数据创建一个新对象

1
2
3
4
5
6
7
function cloneOtherType(target, type) {
switch (type) {
default: {
return new target.constructor(target)
}
}
}

复制正则对象

正则对象的复制方法如下(lodash的实现)

1
2
3
4
5
6
function cloneReg(target) {
const reFlags = /\w*$/;
const result = new targe.constructor(targe.source, reFlags.exec(target));
result.lastIndex = targe.lastIndex;
return result;
}

RegExp构造函数接受两个参数,第一个参数是源码source,第二个参数是修饰符flags

1
2
3
4
5
6
7
const regexp = /xyz/gim;

regexp.source
// => "xyz"

regexp.flags
// => "gim"

JS中的正则对象包括6个修饰符g(全局)/i(大小写)/m(多行)/s(dotAll)/u(Unicode字符)/y(粘滞)(具体的意义参考这篇文章

上面的方法中没有通过flags获取修饰符,而是采取了正则提取。

另外,在克隆正则对象时,还要克隆其lastIndexlastIndex表示每次匹配时的开始位置,当修饰符中包括gy时,会对lastIndex有影响,并且是可写的,所以单独对lastIndex进行了复制

复制Symbol对象

1
2
3
function cloneSymbol(targe) {
return Object(Symbol.prototype.valueOf.call(targe));
}

可以继续继续遍历的类型

数组、对象、Set和Map,是可以继续遍历的类型。对于可以继续遍历的类型,需要获取初始化数据,数组是[],对象是{}

更通用的方法是通过constructor的方式拿到,例如{}new Object()效果是一样的,这样的好处是可以保留被拷贝对象原型上的数据

1
2
3
4
5
6
function getInit(target) {
return new target.constructor();
}

//处理可以继续遍历的类型
const cloneTarget = getInit(target);

初始化完成后,需要分别进行复制操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 克隆 Set
if (type === setTag) {
for (const v of target) {
cloneTarget.add(deepClone(v, cache));
}
}

// 克隆 Map
if (type === mapTag) {
for (const [key, value] of target) {
cloneTarget.set(key, deepClone(value, cache));
}
}

// 克隆对象和数组
if (type === objectTag || type === arrayTag) {
for (const key in target) {
cloneTarget[key] = deepClone(target[key], cache);
}
}

函数的处理

首先通过是否有prototype属性,判断是箭头函数还是普通函数,箭头函数使用eval和函数字符串来声生成一个新的尖头管函数,普通函数需要使用正则提取指出函数体和函数参数,然后使用new Function来生成一个新的函数

这部分省略了,直接将isObject中对于函数的限制放开,因为实际上没有任何的应用场景,因为函数没有必要拷贝,两个对象使用一个在内存中处于同一个地址的函数是没有任何问题的

完整代码

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
function isObject(target) {
return target !== null && typeof target === 'object';
}

function getType(target) {
return Object.prototype.toString.call(target);
}

function getInit(target) {
return new target.constructor();
}

const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';

const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const regexpTag = '[object RegExp]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';

const deepTags = [mapTag, setTag, arrayTag, objectTag];

function cloneSymbol(target) {
return Object(Symbol.prototype.valueOf.call(target));
}

function cloneReg(target) {
const reFlags = /\w*$/;
const result = new target.constructor(target.source, reFlags.exec(target));
result.lastIndex = target.lastIndex;
return result;
}

function cloneOtherType(target, type) {
switch (type) {
case regexpTag: {
return cloneReg(target);
}
case symbolTag: {
return cloneSymbol(target);
}
default: {
return new target.constructor(target);
}
}
}

function deepClone(target, cache = new WeakMap()) {
// 克隆原始类型
if (!isObject(target)) {
return target;
}

// 处理循环引用
const cachedTarget = cache.get(target);
if (cachedTarget) {
return cachedTarget;
}

// 类型判断
const type = getType(target);

// 不可以继续遍历的类型
if (!deepTags.includes(type)) {
return cloneOtherType(target, type);
}

// 处理可以继续遍历的类型
const cloneTarget = getInit(target);
cache.set(target, cloneTarget);

// 克隆 Set
if (type === setTag) {
for (const v of target) {
cloneTarget.add(deepClone(v, cache));
}
}

// 克隆 Map
if (type === mapTag) {
for (const [key, value] of target) {
cloneTarget.set(key, deepClone(value, cache));
}
}

// 克隆对象和数组
if (type === objectTag || type === arrayTag) {
for (const key in target) {
cloneTarget[key] = deepClone(target[key], cache);
}
}

return cloneTarget;
}

总结

总结一下,深拷贝要满足的要求:

  1. 合理处理基本值
  2. 合理处理引用值中的数组、对象、Set、Map
  3. 合理处理Symbol、RegExp对象
  4. 合理处理其他引用类型
  5. 合理处理循环引用的情况
  6. 使用WeakMap进行性能优化

参考