从深拷贝看JS中的循环引用
# 循环引用
在处理 拷贝 函数时,我们要关注几个点
- 循环引用
- 相同引用
- 各种数据类型
如果不处理这些,是没有办法完成深拷贝的操作的,你可能会想,我可以使用 JSON.parse (opens new window) 和 JSON.stringify (opens new window) 来处理啊
使用 JSON 来进行转换,在日常工作中,确实比较常见,但是它也是有限制的,
如果对象中存在时间对象,在通过
JSON.stringify
和JSON.parse
转换后,时间会变成字符串形式,而不是时间对象const obj = { date: new Date } obj.date.getFullYear() // 2021 const temp = JSON.parse(JSON.stringify(obj)) temp.date.getFullYear() // TypeError: temp.date.getFullYear is not a function
1
2
3
4
5
6
7
8
9
10如果对象中存在 RegExp 、 Error 对象,序列化结果只会得到空对象
const obj = { reg: new RegExp('\\d'), err: new Error('Test') } const temp = JSON.parse(JSON.stringify(obj)) // { reg: {}, err: {} }
1
2
3
4
5
6对于循环引用的对象,会抛出
TypeError ("cyclic object value")
还有很多中情况,JSON.stringify
都没有办法完全处理,关于 JSON.stringify
具体可参见 MDN 中对于JSON.stringify的描述 (opens new window)
我们这次首先看看对于 循环引用和相同引用,在不使用 JSON.stringify
的情况下怎么处理
# 首先,什么是循环引用,什么是相同引用
我们以数组举例
const a = [1]
a.push(a)
2
我们让 a
自己 push
了自己,这个时候就出现了循环引用
在这种情况下, a
可以无限展开,我们如果要写递归的话,也是不能处理的,递归这里就会造成死循环。
var arr = [1,2,3]
var obj = {}
obj.a = arr
obj.b = arr
2
3
4
我们让对象中的元素指向同一内存空间,这个时候就造成了相同引用
# 实现一个浅拷贝
const a = [{a: 1}]
const {length} = a
const b = new Array(length)
let index = -1
while (++index < length) {
b[index] = a[index]
}
2
3
4
5
6
7
8
9
10
11
这样就实现了一个简单的浅拷贝
浅拷贝的问题就是在于,当引用类型的值改变时,拷贝出来的对象对应的值也会发生改变,在平时使用过程中,某些数据就是要脱离原先地址,要使用新的地址保存,使其不会污染原数据,所以就需要使用深拷贝来解决这个问题
# 深拷贝
我们以数组举例
const a = [{a: 1}]
我们首先使用 JSON.parse
和 JSON.stringify
来完成一个深拷贝
const b = JSON.parse(JSON.stringify(a))
这个样子会得到我们要的结果,但是前文也说过了,如果放到循环引用和一些特定的类型时,会报错
const a = [1]
a.push(a)
JSON.parse(JSON.stringify(a))
2
3
4
在这个时候我们就不能使用 JSON.parse
的形式来进行 clone
了
# 递归函数
以数组举例
function clone (arr) {
if (!Array.isArray(arr)) return arr
const {length} = arr
const result = new Array(length)
let index = -1
while (++index < length) {
result[index] = clone(arr[index])
}
return result
}
2
3
4
5
6
7
8
9
10
如果当它是数组时,我们递归处理,在上文我们说了递归处理对于普通的数组没有问题,对于循环引用的问题还是没有得到解决
可以看到,报 栈溢出 了
# 那么对于循环引用的问题应该怎么解决
- 我们可以维护一个变量用来缓存已经遍历的值
- 每次递归遍历时,判断当前值是否已经在变量中,如果有,说明已经递归过当前值,直接停止当前递归,返回上次递归的值即可
以数组为例,实现如下
function cloneDeep (arr) {
const map = new Map
function _deep(target) {
if (!Array.isArray(target)) return target
if (map.get(target)) return map.get(target)
const {length} = target
const result = new Array(length)
map.set(target, result)
let index = -1
while (++index < length) {
result[index] = _deep(target[index])
}
return result
}
return _deep(arr)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
通过一个 Map
对象来缓存,在递归调用时,进行一个判断,如果 key
值已经重复了,直接返回对应的 value
即可
如果有处理 数组或者对象等引用类型对比的函数 比如 lodash 的 equalArrays (opens new window) 函数,也是这样的思路来进行循环引用的处理,可以使用 ==
来进行对比
==
会遵循 Abstract Equality Comparison (opens new window) 判断逻辑,在类型相同时,会执行 Strict Equality Comparison (opens new window) 判断逻辑
最终如果都是引用类型会走到 SameValueNonNumber (opens new window) 规范,该规范规定 如果 x 和 y 指向同一个对象,返回 true, 否则返回 false
至此,就解决了循环引用和相同引用的问题
关于其他类型的处理逻辑,可以参考 lodash baseClone 的实现 (opens new window)