徐漂漂

☝️ 一个工作 2 年并无建树的前端搬砖师

0%

如何实现一个深拷贝

这部分我是看了 lodash 的相关源码,它真的实现得非常完整!
总结成一句话,就是需要考虑很多数据类型,然后针对这些数据类型拷贝就行了😏

对于引用类型来说,我们基本可以按照以下思路走:

  1. 初始化。即调用相应的构造函数
  2. 递归地赋值
  3. 有循环引用的话需要处理一下

1. 拷贝基本类型

基本类型直接赋值就可以。

1
2
3
4
5
6
7
8
9
10
11
12
function deepClone (value) {
// 基本类型
if (!isObject(value)) {
return value
}
}

// 判断是不是对象
function isObject(value) {
const type = typeof value
return value != null && (type === 'object' || type === 'function')
}

接下来是怎么拷贝引用类型。我会按照以下顺序来介绍:

  1. 数组
  2. 函数
  3. 对象
  4. 特殊类型。Boolean、Date、Map、Number 等等

另外,lodash 还实现了 Buffer(node.js)等拷贝,但我实际用得不多,就不展开了,有兴趣的可以去看看源码。

2. 拷贝数组

2.1 初始化

先初始化一个长度为原数组长度的数组

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
export function deepClone(value) {
let result

// 基本类型
if (!isObject(value)) {
return value
}

const isArr = Array.isArray(value)

// 数组
if (isArr) {
result = initCloneArray(value)
}
// 待续
}

const hasOwnProperty = Object.prototype.hasOwnProperty

// 数组初始化
function initCloneArray(array) {
const { length } = array
const result = new array.constructor(length)

// 因为 RegExp.prototype.exec() 会返回一个数组或 null,这个数组里有两个特殊的属性:input、index
// 类似 ["foo", index: 6, input: "table football, foosball", groups: undefined]
// 所以需要进行特殊处理
if (length && typeof array[0] === 'string' && hasOwnProperty.call(array, 'index')) {
result.index = array.index
result.input = array.input
}
return result
}

2.2 赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export function deepClone(value) {
let result

// 基本类型
if (!isObject(value)) {
return value
}

const isArr = Array.isArray(value)

// 数组
if (isArr) {
result = initCloneArray(value)
}

// 赋值
if (isArr) {
for (let i = 0; i< value.length; i++) {
result[i] = deepClone(value[i])
}
}

return result
}

3. 拷贝函数

函数的拷贝的话,我们还是返回之前的引用。

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
export function deepClone(value) {
let result

// 基本类型
if (!isObject(value)) {
return value
}

const isArr = Array.isArray(value)

// 数组
if (isArr) {
result = initCloneArray(value)
} else {
const isFunc = typeof value === 'function'
// 函数
if (isFunc) {
return value
}
}

// 赋值
if (isArr) {
for (let i = 0; i< value.length; i++) {
result[i] = deepClone(value[i])
}
}

return result
}

4. 拷贝对象

初始化一个对象,然后赋值。
要注意的是这个拷贝后的对象和原对象的原型链是一样的

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
function deepClone(value) {
let result

// 基本类型
if (!isObject(value)) {
return value
}

const isArr = Array.isArray(value)
const tag = getTag(value)

// 数组
if (isArr) {
result = initCloneArray(value)
} else {
const isFunc = typeof value === 'function'
// 函数
if (isFunc) {
return value
}

// 对象或 arguments
if (tag == '[object Object]' || tag == '[object Arguments]') {
result = initCloneObject(value)
}
}

if (isArr) {
// 数组赋值
for (let i = 0; i< value.length; i++) {
result[i] = deepClone(value[i])
}
} else {
// 对象赋值
Object.keys(Object(value)).forEach(k => {
result[k] = deepClone(value[k])
})
}

return result

}

// 能更细致地判断是什么类型
function getTag(value) {
if (value == null) {
return value === undefined ? '[object Undefined]' : '[object Null]'
}
return toString.call(value)
}

const objectProto = Object.prototype
function isPrototype(value) {
const Ctor = value && value.constructor
const proto = (typeof Ctor === 'function' && Ctor.prototype) || objectProto

return value === proto
}

// 初始化对象
function initCloneObject(object) {
return (typeof object.constructor === 'function' && !isPrototype(object))
? Object.create(Object.getPrototypeOf(object))
: {}
}

5. 拷贝特殊对象

包括 Boolean, Date, Map, Number, RegExp, Set, String, Symbol

接下来的思路也是一样的,先调用对应的构造函数。然后赋值就行了。稍微麻烦一点的可能是 Regexp 正则对象和 Symbol 对象

5.1 初始化

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
function deepClone(value) {
let result

// 基本类型
if (!isObject(value)) {
return value
}

const isArr = Array.isArray(value)
const tag = getTag(value)

// 数组
if (isArr) {
result = initCloneArray(value)
} else {
const isFunc = typeof value === 'function'
// 函数
if (isFunc) {
return value
}

// 对象或 arguments
if (tag == '[object Object]' || tag == '[object Arguments]') {
result = initCloneObject(value)
} else {
// 特殊对象的初始化
result = initCloneByTag(value, tag)
}
}

if (isArr) {
// 数组赋值
for (let i = 0; i< value.length; i++) {
result[i] = deepClone(value[i])
}
} else {
// 对象赋值
Object.keys(Object(value)).forEach(k => {
result[k] = deepClone(value[k])
})
}

return result

}

const toString = Object.prototype.toString
// 能更细致地判断是什么类型
function getTag(value) {
if (value == null) {
return value === undefined ? '[object Undefined]' : '[object Null]'
}
return toString.call(value)
}

const objectProto = Object.prototype
function isPrototype(value) {
const Ctor = value && value.constructor
const proto = (typeof Ctor === 'function' && Ctor.prototype) || objectProto

return value === proto
}

// 特殊对象的初始化
function initCloneByTag(object, tag, isDeep) {
const Ctor = object.constructor
switch (tag) {

case '[object Boolean]':
case '[object Date]':
return new Ctor(+object)

case '[object Set]':
case '[object Map]':
return new Ctor

case '[object Number]':
case '[object String]':
return new Ctor(object)

case '[object RegExp]':
return cloneRegExp(object)

case '[object Symbol]':
return cloneSymbol(object)
}
}

const reFlags = /\w*$/
// 拷贝一个正则
function cloneRegExp(regexp) {
// RegExp 构造函数有两个参数。pattern(正则表达式的文本。),flags(标志)
// source 属性返回一个值为当前正则表达式对象的模式文本的字符串,该字符串不会包含正则字面量两边的斜杠以及任何的标志字符。
// reFlags.exec(regexp) 实际上是 reFlags.exec(regexp.toString())。提取出了标志字符
const result = new regexp.constructor(regexp.source, reFlags.exec(regexp))
result.lastIndex = regexp.lastIndex
return result
}

const symbolValueOf = Symbol.prototype.valueOf
// 拷贝一个 Symbol
function cloneSymbol(symbol) {
return Object(symbolValueOf.call(symbol))
}

5.2 赋值

虽然是特殊对象,但也是对象,所以我们的思路还是获取该对象的所有属性,然后赋值就可以了。
需要注意的是

  1. Object.keys 不能获取 Symbol 属性,可以再加上 Object.getOwnPropertySymbols()来获取所有 Symbol 属性名
  2. Set 和 Map 的赋值是通过 add 和 set 来的
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
function deepClone(value) {
let result

// 基本类型
if (!isObject(value)) {
return value
}

const isArr = Array.isArray(value)
const tag = getTag(value)

// 数组
if (isArr) {
result = initCloneArray(value)
} else {
const isFunc = typeof value === 'function'
// 函数
if (isFunc) {
return value
}

// 对象或 arguments
if (tag == '[object Object]' || tag == '[object Arguments]') {
result = initCloneObject(value)
} else {
// 特殊对象的初始化
result = initCloneByTag(value, tag)
}
}

// Map 赋值
if (tag === mapTag) {
value.forEach((subValue, key) => {
result.set(key, deepClone(subValue))
})
return result
}

// Set 赋值
if (tag === setTag) {
value.forEach((subValue) => {
result.add(deepClone(subValue))
})
return result
}

if (isArr) {
// 数组赋值
for (let i = 0; i< value.length; i++) {
result[i] = deepClone(value[i])
}
} else {
// 对象赋值
Object.keys(Object(value)).forEach(k => {
result[k] = deepClone(value[k])
})

const propertyIsEnumerable = Object.prototype.propertyIsEnumerable

// 过滤掉不可枚举的 Symbol 属性并赋值
Object.getOwnPropertySymbols(value)
.filter((symbol) => propertyIsEnumerable.call(value, symbol))
.forEach(k => {
result[k] = deepClone(value[k])
})
}

return result

}

6. 测试

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
const set = new Set()
set.add('gioia')
set.add('me')

const map = new Map()
map.set(0, 'zero')
map.set(1, 'one')

const original = {
name: 'gioia',
getName: function () {
return this.name
},
[Symbol()]: 'symbol prop',
sym: Symbol('symbol value'),
friends: [{
name: 'xkld',
}, 'cln'],
dress: {
pants: {
color: 'black'
},
shirts: {
colors: ['blue', 'white']
}
},
map: map,
set: set
}

const copy = deepClone(original)
console.log(copy)
console.log(copy.sym === original.sym)
console.log(copy.friends === original.friends)
console.log(copy.map === original.map)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
name: 'gioia',
getName: [Function: getName],
sym: Symbol(symbol value),
friends: [ { name: 'xkld' }, 'cln' ],
dress: { pants: { color: 'black' }, shirts: { colors: [ 'blue', 'white' ] } },
map: Map { 0 => 'zero', 1 => 'one' },
set: Set { 'gioia', 'me' },
[Symbol()]: 'symbol prop'
}

true
false
false

7. 解决循环引用

以上我们已经初步实现了一个深拷贝了。但是在循环引用的场景下,会出现栈溢出的现象。
例如 original.circle = original 这种情况,我们要是还递归地赋值的话,就永远也没有尽头🥱
解决办法就是,看看我们要拷贝的对象之前有没有处理过,有的话就直接引用就行了;没有的话再进行赋值并记录在案。你可以选择很多存储方案,像 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
74
75
76
77
78
function deepClone(value) {
let result

// 基本类型
if (!isObject(value)) {
return value
}

const isArr = Array.isArray(value)
const tag = getTag(value)

// 数组
if (isArr) {
result = initCloneArray(value)
} else {
const isFunc = typeof value === 'function'
// 函数
if (isFunc) {
return value
}

// 对象或 arguments
if (tag == '[object Object]' || tag == '[object Arguments]') {
result = initCloneObject(value)
} else {
// 特殊对象的初始化
result = initCloneByTag(value, tag)
}
}

// 检查循环引用并返回其对应的拷贝
cache || (cache = new Map())
const cached = cache.get(value)
if (cached) {
return cached
}
cache.set(value, result)

// Map 赋值
if (tag === mapTag) {
value.forEach((subValue, key) => {
result.set(key, deepClone(subValue))
})
return result
}

// Set 赋值
if (tag === setTag) {
value.forEach((subValue) => {
result.add(deepClone(subValue))
})
return result
}

if (isArr) {
// 数组赋值
for (let i = 0; i< value.length; i++) {
result[i] = deepClone(value[i])
}
} else {
// 对象赋值
Object.keys(Object(value)).forEach(k => {
result[k] = deepClone(value[k])
})

const propertyIsEnumerable = Object.prototype.propertyIsEnumerable

// 过滤掉不可枚举的 Symbol 属性并赋值
Object.getOwnPropertySymbols(value)
.filter((symbol) => propertyIsEnumerable.call(value, symbol))
.forEach(k => {
result[k] = deepClone(value[k])
})
}

return result

}