1. shallow clone

JavaScript原始类型: Undefined、Null、Boolean、Number、String、Symbol(栈内存)
JavaScript引用类型:Object (堆内存)

浅克隆之所以被称为浅克隆,是因为对象只会被克隆最外部的一层,至于更深层的对象,则依然是通过引用指向同一块堆内存.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// shallow_clone.ts
export const shallowClone = (object: any) => {
const newObject = {};
for (const key in object) {
if (object.hasOwnProperty(key)) {
newObject[key] = object[key];
}
}
return newObject;
}
// shallow_clone.test.ts
import { shallowClone } from "../../util/clone";

it("new object in deep object should be the same", () => {
const mockData = {
a: 1,
b: [1, 2, 3],
c: { d: 4 }
}
const newObject: any = shallowClone(mockData);
expect(newObject).toEqual(mockData);
expect(newObject).not.toBe(mockData);
expect(newObject.c.d).toBe(mockData.c.d); // 更深层的对象,则依然是通过引用指向同一块堆内存
})

2. JSON.parse method

便捷实现深克隆的方法, JSON对象parse方法可以将JSON字符串反序列化成JS对象,stringify方法可以将JS对象序列化成JSON字符串,这两个方法结合起来就能产生一个便捷的深克隆.

1
2
3
4
5
6
7
8
9
10
11
12
export const jsonClone = (object: any) => {
return JSON.parse(JSON.stringify(object));
}
it("new object in deep object should not be the same", () => {
const mockData = {
a: 1,
b: [1, 2, 3],
c: { d: { e: 4 } }
}
const newObject: any = jsonClone(mockData);
expect(newObject.c.d).not.toBe(mockData.c.d);
})

这个方法的缺点:

  • 他无法实现对函数 、RegExp等特殊对象的克隆
  • 会抛弃对象的constructor,所有的构造函数会指向Object
  • 对象有循环引用,会报错
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
it("clone complex object like RegExp", () => {
// constructor
function person(name) {
this.name = name;
}
const personInstance = new person("test");
const mockData = {
a: /^[1-9]/i,
b: new RegExp("^[a-z]", "g"),
c: [1],
d: new Array(1), // 稀疏数组,
e: personInstance,
f: person
}
const newObject = jsonClone(mockData);
/**
* old object
{ a: /^[1-9]/i,
b: /^[a-z]/g,
c: [ 1 ],
d: [ <1 empty item> ],
e: person { name: 'test' },
f: [Function: person] }
* new object
{ a: {}
b: {}, // RegExp
c: [ 1 ],
d: [ null ], // Array
e: { name: 'test' }, // constructor
f: undefind } // function
}
*/
expect(newObject.a).toEqual({});
expect(newObject.b).toEqual({});
expect(newObject.c[0]).toBe(1);
expect(newObject.d[0]).toBe(null);
expect(mockData.d[0]).toBe(undefined);
})

对象的循环引用会抛出错误

1
2
3
4
5
6
const oldObject = {};

oldObject.a = oldObject;

const newObject = JSON.parse(JSON.stringify(oldObject));
console.log(newObject.a, oldObject.a); // TypeError: Converting circular structure to JSON

与JSON.parse clone效果差不多的clonedeep方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export const cloneDeep = (object: any) => {
if (typeof object !== "object") return object;
const newObject = object instanceof Array ? [] : {};
for (const key in object) {
if (object.hasOwnProperty(key)) {
if (typeof object[key] === "object" && object[key] !== null) {
newObject[key] = cloneDeep(object[key]);
} else {
newObject[key] = object[key];
}
}
}
return newObject
}

3. deep clone

由于要面对不同的对象(正则、数组、Date等)要采用不同的处理方式,我们需要实现一个对象类型判断函数。

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
100
101
102
103
104
105
106
107
108

// 对特殊对象进行类型判断
const isType = (object, type): boolean => {
if (typeof object !== "object") return false;
const typeString = Object.prototype.toString.call(object);
switch (type) {
case "Array":
return typeString === "[object Array]";
case "Date":
return typeString === "[object Date]";
case "RegExp":
return typeString === "[object RegExp]";
default:
return false;
}
}

// 实现一个提取flags的函数
const getRegExpFlags = (regExp: RegExp) => {
let flags = "";
if (regExp.global) flags += "g";
if (regExp.ignoreCase) flags += "i";
if (regExp.multiline) flags += "m";
return flags;
}
// Buffer对象、Promise、Set、Map暂未处理
export const deepClone = oldObject => {
// 维护两个储存循环引用的数组
const oldObjects = [];
const newObjects = [];

const _deepClone = oldObject => {
// 递归直到oldobject为null时,或类型不为“object”时停止。
if (oldObject === null) return null;
if (typeof oldObject !== 'object') return oldObject;

let newObject, newProtoType;

if (isType(oldObject, 'Array')) {
// 对数组做特殊处理
newObject = [];
} else if (isType(oldObject, 'RegExp')) {
// 对正则对象做特殊处理
newObject = new RegExp(oldObject.source, getRegExpFlags(oldObject));
if (oldObject.lastIndex) newObject.lastIndex = oldObject.lastIndex;
} else if (isType(oldObject, 'Date')) {
// 对Date对象做特殊处理
newObject = new Date(oldObject.getTime());
} else {
// 处理对象原型
newProtoType = Object.getPrototypeOf(oldObject);
// 利用Object.create切断原型链
newObject = Object.create(newProtoType);
}

// 处理循环引用
const index = oldObjects.indexOf(oldObject);

if (index != -1) {
// 如果父数组存在本对象,说明之前已经被引用过,直接返回此对象
return newObjects[index];
}
oldObjects.push(oldObject);
newObjects.push(newObject);

for (const key in oldObject) {
if (oldObject.hasOwnProperty(key)) {
// newObject 已根据条件做了特殊处理
newObject[key] = _deepClone(oldObject[key]);
}
}

return newObject;
};
return _deepClone(oldObject);
};
// test
it("test deep clone method", () => {
// constructor
function person(name) {
this.name = name;
}
const personInstance = new person("test");
const mockData = {
a: null,
b: [1, 2, 3],
c: { d: { e: 4 } },
f: new RegExp("^[a-z]", "g"),
h: new Array(1, 4, 6),
i: personInstance,
j: person,
l: "test",
m: new Array(2)
}
const newObject: any = deepClone(mockData);
console.log(newObject)
/**
* { a: null,
b: [ 1, 2, 3 ],
c: { d: { e: 4 } },
f: /^[a-z]/g,
h: [ 1, 4, 6 ],
i: person { name: 'test' },
j: [Function: person],
l: 'test',
m: [] }
*/
})

对于Buffer对象、Promise、Set、Map可能都需要我们做特殊处理,另外对于确保没有循环引用的对象,我们可以省去对循环引用的特殊处理,因为这很消耗时间,不过一个基本的深克隆函数我们已经实现了,在生产环境中最好用lodash的深克隆实现.