深拷贝使用场景:
- 默认选项
- 部分修改
API: 接收一个对象或者基本类型,对象的类型包括 Array、function、普通 Object、RegExp 正则表达式、Date
返回一个和接收对象一样的数据,但是两者之间没有任何节点有联系,修改 A 不会影响 B
版本 0.1 最简单版本(直接一把梭)
直接使用 JSON 序列化
1
2
3
4
|
let a = { a: 1, b: 2, c: 3 }
let b = JSON.parse(JSON.stringify(a))
console.log(a)
console.log(b)
|
缺点:
1.不支持函数,比如对象里有有一个 key,value 的 value 是一个函数
2.不支持 undefined
3.JSON 只支持树状的结构不支持环状的结构,如果原对象有一个对自己的引用,JSON 的序列化会报错
4.不支持 Date、正则表达式、Symbol、Set、Map(综上就是所有 JSON 不支持的数据类型都不支持)
其他的浅拷贝办法:
1.Object.assign({}, a)
2.{…a}
版本 1 我能拷贝有不同对象的数据了
可以看到 JSON 序列化的深拷贝方式已经不满足了,
那么现在我们得手动实现一个深拷贝了,我们用递归的形式来实现
在递归的时候一定会遇到不同的数据类型,要按照不用的数据类型来分类讨论
js 一共有 8 种数据类型,
如果是 7 种基本类型之一就直接 return
undefined
null
boolean
number
string
symbol
bigint
如果是 object 就再分类讨论,不同的类型用不同的构造方法去构造一个新的对象
- Array 就遍历复制属性到一个新的数组 new Array
- function 就在一个新的 function 内 source.apply(this, arguments) //source 是要拷贝的对象
再遍历 source 的属性到这个新的 function
- Object 也 遍历复制 key value 到一个新对象 new Object
- RegExp 正则表达式 数据类型
- Date 数据类型
其中所有的复制都用 deepClone 这个函数自己递归
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
function deepClone(source) {
if (source instanceof Object) {
let dist
if (source instanceof Array) {
dist = new Array()
} else if (source instanceof Function) {
dist = function () {
return source.apply(this, arguments)
}
} else if (source instanceof RegExp) {
dist = new RegExp(source.source, source.flags)
} else if (source instanceof Date) {
dist = new Date(source)
} else {
dist = new Object()
}
for (let key in source) {
dist[key] = deepClone(source[key])
}
return dist
}
return source
}
|
版本 2 我能拷贝具有环结构的数据了
到目前为止一切顺利,也没有什么大问题
因为要拷贝的对象的数据是有限的 deepClone 的递归会自动结束,不用特地给一个出口
但是考虑到一种情况,在讨论 JSON 序列化深拷贝的时候第三种情况是拷贝环结构,
即一个对象的某个属性引用了自身,比如:window.self === window
我们目前构建的 deepClone 方法,在遇到有环结构的对象时会不断递归永远没有出口结束
数组也可能存在环结构:
函数也可能存在环结构:
1
2
3
4
|
function x() {
return x()
}
x()
|
解决办法是使用缓存来标记已经复制过的对象
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
|
let cache = []
function deepClone(source) {
if (source instanceof Object) {
let cacheDist = findCache(source)
// 有缓存
if (cacheDist) {
return cacheDist
} else {
//无缓存
let dist
if (source instanceof Array) {
dist = new Array()
} else if (source instanceof Function) {
dist = function () {
return source.apply(this, arguments)
}
} else if (source instanceof RegExp) {
dist = new RegExp(source.source, source.flags)
} else if (source instanceof Date) {
dist = new Date(source)
} else {
dist = new Object()
}
// 加入缓存
cache.push([source, dist])
for (let key in source) {
dist[key] = deepClone(source[key])
}
return dist
}
}
// 如果是基本类型就直接return
return source
}
// 查看当前的对象是否已经在缓存中
function findCache(source) {
for (let i = 0; i < cache.length; i++) {
if (cache[i][0] === source) {
return cache[i][1]
}
}
return undefined
}
|
版本 3 我需要跳过原型再拷贝
现在具有环结构的对象现在也可以复制了。
此外还有一个坑,我们知道每一个对象都有自己的原型,__proto__
属性
目前还没考虑是否要拷贝对象的原型(这里原型很可能还有原型是很深的一个对象属性)
一般来说我们不需要拷贝 proto 这个属性,因为原型里的所有方法和对象都有各自的原型,如果我们要决定拷贝原型
就意味着要把 js 中的很多基本原型都拷贝一遍,所以这里一般不需要拷贝对象的原型,所以需要绕开它
1
2
3
4
5
6
|
for (let key in source) {
// 只复制对象自己的属性,跳过原型的属性
if (source.hasOwnProperty(key)) {
dist[key] = deepClone(source[key])
}
}
|
我们修改一下代码,下面是完整代码
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
|
let cache = []
function deepClone(source) {
if (source instanceof Object) {
let cacheDist = findCache(source)
// 有缓存
if (cacheDist) {
return cacheDist
} else {
//无缓存
let dist
if (source instanceof Array) {
dist = new Array()
} else if (source instanceof Function) {
dist = function () {
return source.apply(this, arguments)
}
} else if (source instanceof RegExp) {
dist = new RegExp(source.source, source.flags)
} else if (source instanceof Date) {
dist = new Date(source)
} else {
dist = new Object()
}
// 加入缓存
cache.push([source, dist])
for (let key in source) {
// 只复制对象自己的属性,跳过原型的属性
if (source.hasOwnProperty(key)) {
dist[key] = deepClone(source[key])
}
}
return dist
}
}
// 如果是基本类型就直接return
return source
}
// 查看当前的对象是否已经在缓存中
function findCache(source) {
for (let i = 0; i < cache.length; i++) {
if (cache[i][0] === source) {
return cache[i][1]
}
}
return undefined
}
|
版本 4 强化拷贝函数,最后的力量!
你或许有注意到,目前在函数外面有一个 cache 用于存放深拷贝时的缓存,我们在调用完后还得清空它这样很麻烦
我们尝试写成一个 class,来包含 cache 变量和帮助函数 findCache
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
|
class DeepClone {
cache
constructor() {
this.cache = []
}
clone(source) {
if (source instanceof Object) {
let cacheDist = this.findCache(source)
// 有缓存
if (cacheDist) {
return cacheDist
} else { //无缓存
let dist
if (source instanceof Array) {
dist = new Array()
} else if (source instanceof Function) {
dist = function () {
return source.apply(this, arguments)
}
} else if (source instanceof RegExp) {
dist = new RegExp(source.source, source.flags)
} else if (source instanceof Date) {
dist = new Date(source)
} else {
dist = new Object()
}
// 加入缓存
this.cache.push([source, dist])
for (let key in source) {
// 只复制对象自己的属性,跳过原型的属性
if (source.hasOwnProperty(key)) {
dist[key] = this.clone(source[key])
}
}
return dist
}
}
// 如果是基本类型就直接return
return source
}
// 查看当前的对象是否已经在缓存中
findCache(source) {
for (let i = 0; i < this.cache.length; i++) {
if (this.cache[i][0] === source) {
return this.cache[i][1]
}
}
return undefined
}
}
const a = {
self: a
child: [
a: undefined,
b: new Date()
c: new RegExp("hi\\d+", "gi")
]
}
const b = new DeepClone().clone(a)
|
什么?最后的魔王
有一种情况无论是在浏览器还是在 nodejs 环境中都会存在
当对象有子属性,子属性还有子属性。。。对象的结构非常深的时候会导致递归调用很多次,会出现函数执行栈超出最大值的情况
(爆栈:RangeError: Maximum call stack size exceeded)
这里设想的办法是把对象的纵向的结构换成横向的结构(放在类似数组的结构里)这样用 for 循环来避免递归过于深的情况
暂时不知道怎么解
我选择真香
自己写的 deepClone 总会有考虑不周全的地方
实际工作中可以借助第三方:
// Lodash
Lodash.cloneDeep()
// 干脆不用深拷贝了,使用部分修改
immutable.js 部分修改
immer.js