前言
大家好,我是CoderBin。手写实现一个深拷贝函数几乎是前端开发人员的必备技能之一(面试😒),如果已经掌握了的可以把本文当做复习,如果还没掌握深拷贝如何实现的,建议学习起来啦。本次会用两种方式实现深拷贝:JSON.stringify、利用递归。
首先大家应该都了解过,使用 JSON.stringify 实现递归是有一定缺陷的,并不是深拷贝函数实现的最优解。本文先使用这种方式实现深拷贝,再指出它到底有什么缺点,然后再使用递归去一步步解决这些缺点,进一步优化深拷贝函数。希望对大家有所帮助,谢谢 💗
如果文中有不对、疑惑的地方,欢迎在评论区留言指正🌻
1. 深拷贝浅拷贝的区别
在实现深拷贝前,首先要知道深拷贝与浅拷贝的区别,这里只说最重要的一点:
-
浅拷贝是拷贝地址,如果修改新对象里面值为对象的某个值,原本的对象也会被修改,原因是它们共享一份地址所指向的数据
-
深拷贝会直接开辟一个新的空间,将数据拷贝进来。这样新旧对象互不相干,不会有浅拷贝的问题。
浅拷贝实现,具体代码:
const obj = {
name: 'CoderBin',
friend: {
name: 'Jack'
}
}
const newObj = Object.assign({}, obj)
newObj.friend.name = 'Tom'
console.log(obj)
这里使用 Object.assign() 实现浅拷贝,紧接着去修改新对象 newObj.friend.name 的值,然后输出原对象,结果如图:
可以看到,明明修改的是新对象的数据,可原对象
obj.friend.name 的值也被改为了 Tom,这就说明了浅拷贝只实现了外层的拷贝。接下来看看深拷贝的实现是否会有这个问题。
2. JSON.stringify实现深拷贝
实际上,是借用了 JSON 的两个API JSON.stringify() 和 JSON.parse() 来实现深拷贝,具体代码:
const obj = {
name: 'CoderBin',
friend: {
name: 'Jack'
}
}
const newObj = JSON.parse(JSON.stringify(obj))
newObj.friend.name = 'Tom'
console.log(obj)
这里只修改了上面浅拷贝实现的一行代码,使用JSON的两个API实现深拷贝,让我们来看看结果会如何:
可以看到,修改了新对象的数据,原对象 newObj.friend.name 的值并没有被改变。
3. JSON.stringify的缺陷
使用JSON两个API实现的深拷贝只是较为浅显的实现方法,原因是这种方法还有较多的弊端
3.1 缺陷一:部分数据类型拷贝有误
在拷贝部分数据类型时,会有意想不到的情况发生,具体代码:
const s1 = Symbol('s1')
const s2 = Symbol('s2')
const obj = {
name: 'CoderBin',
friend: {
name: 'Jack'
},
// undefined 类型
b: undefined,
// 函数类型
foo: () => { },
// Symbol作为键值
[s1]: 's11',
s2: s2,
// Set 类型
set: new Set(['a', 'b', 'c']),
// Map 类型
map: new Map([
['a', 'aa'],
['b', 'bb']
])
}
const newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)
上面在原对象中增加了undefined、函数、Set、Map类型的数据和Symbol作为键值的属性值,然后通过 JSON.stringify 实现深拷贝,输出新对象,看看输出结果如何:
可以看到,深拷贝出来的新对象数据明显和原对象有区别:
- 省略了undefined、函数类型的数据
- 省略了Symbol作为键值的数据
- Set、Map类型的数据为空
3.2 缺陷二:循环引用报错
在拷贝到循环引用时,会直接报错,具体代码:
const obj = {
name: 'CoderBin',
friend: {
name: 'Jack'
}
}
// 循环引用
obj.info = obj
const newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)
上面代码中 obj.info = obj 就是一个循环引用,拷贝结果:
可以看到,在拷贝过程中直接报错。解决循环引用的问题也是实现深拷贝的一个重点,这个问题会稍微复杂一点,所以放在最后再去解决。
4. 递归实现深拷贝
这里先使用递归对深拷贝进行基本实现,也就是完成JSON.stringify方法实现深拷贝的功能,其他问题在后面慢慢优化解决,具体代码:
// 判断是否为对象
function isObject(value) {
const valueType = typeof value
return (value !== null) && (valueType === "object" || valueType === "function")
}
// 深拷贝函数
function deepClone(originValue) {
if (!isObject(originValue)) {
return originValue
}
// 创建新对象
const newObj = {}
// 遍历原对象
for (const key in originValue) {
newObj[key] = deepClone(originValue[key])
}
return newObj
}
// 原对象
const obj = {
name: 'CoderBin',
friend: {
name: 'Jack'
}
}
const newObj = deepClone(obj)
newObj.friend.name = 'Tom'
console.log(obj)
代码虽然看起来有点复杂,但其实很简单,有如下分析:
1. 两个函数:
- 先封装了个判断是否为对象的函数
isObject,作用是判断每个原对象的属性值是否为对象类型 - 深拷贝函数
deepClone
2. 深拷贝函数 deepClone 实现流程:
- 先判断原对象的属性值是否为对象类型
- 创建新对象,最后return
- 遍历原对象,往新对象上添加原对象的所有属性值。新对象的属性值都为
isObject函数返回的值。
输出结果:
可以看到,修改了新对象的数据,原对象
newObj.friend.name 的值并没有被改变。
5. 深拷贝函数优化-数据类型
仅仅是上面的代码实现一个完整的深拷贝函数是不够的,依旧会存在部分数据类型拷贝失败的情况,原对象代码:
// 原对象
const s1 = Symbol('s1')
const s2 = Symbol('s2')
const obj = {
name: 'CoderBin',
friend: {
name: 'Jack'
},
b: undefined,
s2: s2,
// 以下待优化
skill: ['js', 'vue', 'react'],
foo: () => { },
[s1]: 's11',
set: new Set(['a', 'b', 'c']),
map: new Map([
['a', 'aa'],
['b', 'bb']
])
}
通过深拷贝函数拷贝后的新对象:
使用深拷贝函数创建的新对象主要有如下问题:
- 数组类型拷贝格式错误
- 函数类型拷贝为空对象
- 当Symbol作为键时,会被忽略拷贝
- Set类型拷贝为空对象
- Map类型拷贝为空对象
下面就对以上存在的问题进行优化
5.1 优化数组类型拷贝
首先要弄清楚为什么深拷贝出来的数组会是那种情况,原因如下:
- 新建对象
newObj时,定为了{} - 使用
for...in遍历数组时,key为元素索引
解决方法:只需要在新建对象newObj时,进行三元表达式判断即可。判断 originValue 是否为数组,是则赋值空数组,否则赋值空对象。具体修改代码有:
// 深拷贝函数
function deepClone(originValue) {
...
// 创建新对象
const newObj = Array.isArray(originValue) ? [] : {}
...
}
拷贝结果:
5.2 优化函数类型拷贝
由上面可知,当拷贝函数类型的值时,会变成空对象。解决的方法比较简单,只需要在前面判断值是否为函数类型,是则直接返回,具体代码:
// 深拷贝函数
function deepClone(originValue) {
if (typeof originValue === 'function') {
return originValue
}
...
return newObj
}
拷贝结果如下:
可以看到,已经成功拷贝到属性
foo 属性的函数类型的值了,并且也是可以正常调用的。
5.3 优化Symbol类型拷贝
由上面可知,当Symbol作为键时,会被忽略拷贝,解决这个问题的步骤如下:
- 先判断值是否为
symbol类型,是则返回 - 对 symbol类型的 key做特殊处理
// 深拷贝函数
function deepClone(originValue) {
//判断 originVlaue 是否为 symbol 类型
if (typeof originValue === 'symbol') {
return Symbol(originValue.description)
}
...
//对 symbol类型的 key做特殊处理
const symbolKeys = Object.getOwnPropertySymbols(originValue)
for (const skey of symbolKeys) {
newObj[skey] = deepClone(originValue[skey])
}
return newObj
}
Object.getOwnPropertySymbols()方法返回一个给定对象自身的所有 Symbol 属性的数组。 拷贝结果:
可以看到,当
symbol 类型的数据作为键时,也可以成功拷贝了。
5.4 优化Set类型拷贝
由上面可知,当拷贝 Set 类型的值时,会变成空对象。解决的方法比较简单,只需要在前面判断值是否为 Set 类型,是则返回一个新的set数据,再把参数传进去,代码如下:
// 深拷贝函数
function deepClone(originValue) {
//TODO 判断 originValue 是否为 Set 类型
if (originValue instanceof Set) {
return new Set([...originValue])
}
...
return newObj
}
使用 instanceof 方法来判断 originValue 是否为 Set 类型,拷贝结果如下:
可以看到,
Set 类型的数据也可以成功拷贝了。
5.5 优化Map类型拷贝
由上面可知,当拷贝 Map 类型的值时,会变成空对象。解决的方法比较简单,只需要在前面判断值是否为 Map 类型,是则返回一个新的set数据,再把参数传进去,具体代码:
// 深拷贝函数
function deepClone(originValue) {
//TODO 判断 originValue 是否为 Set 类型
if (originValue instanceof Set) {
return new Set([...originValue])
}
...
return newObj
}
使用 instanceof 方法来判断 originValue 是否为 Map 类型,拷贝结果:
可以看到,
Map 类型的数据也可以成功拷贝了。
6. 深拷贝函数优化-循环引用
以目前的深拷贝函数,如果遇到循环引用问题依旧会报错,具体代码:
// 深拷贝函数
function deepClone(originValue) {
...
}
// 原对象
const s1 = Symbol('s1')
const s2 = Symbol('s2')
const obj = {
name: 'CoderBin',
friend: {
name: 'Jack'
},
b: undefined,
s2: s2,
// 待优化
skill: ['js', 'vue', 'react'],
foo: () => { },
[s1]: 's11',
set: new Set(['a', 'b', 'c']),
map: new Map([
['a', 'aa'],
['b', 'bb']
])
}
// 循环引用
obj.info = obj
const newObj = deepClone(obj)
console.log(newObj)
上述代码在倒数第三行进行了循环引用,下面看看拷贝结果如何:
毫无意外的直接报错了。
6.1 出现循环引用问题的原因
会出现该问题的原因是,出现了这行代码obj.info = obj,当深拷贝函数通过递归拷贝数据时,由于 obj.info 的值为 obj,而 obj 里面又有 info 属性,值又是 obj,就这样形成了一个闭环,永远不会结束。
这样就出现了循环引用的问题
6.1 使用map解决循环引用
根据上面的循环引用原因分析,想解决这个问题也不难,只需要保证递归函数内生成的 newObj 只被创建一次,当第二次循环时就把第一次创建的 newObj 给返回出去。
那么如何将 newObj 保存下来,并且可以判断值是否存在,可以取值呢?
答案是利用 Map,使用其两个API进行操作:
map.has(val)用于判断val是否已存在于map中map.get(val)用于取出val对应的值
具体实现代码如下:
// 深拷贝函数
function deepClone(originValue, map = new Map()) {
// 如果已存在新对象,就将之返回
if (map.has(originValue)) {
return originValue
}
// 创建新对象
const newObj = Array.isArray(originValue) ? [] : {}
// 遍历原对象
for (const key in originValue) {
newObj[key] = deepClone(originValue[key], map)
}
//对 symbol类型的 key做特殊处理
const symbolKeys = Object.getOwnPropertySymbols(originValue)
for (const skey of symbolKeys) {
newObj[skey] = deepClone(originValue[skey], map)
}
// 保存第一次对象
map.set(originValue, newObj)
return newObj
}
注意:在进行递归时,要将map当做参数传过去,这样可以保持map的延续使用
最后拷贝结果:
7. 完整代码
/** 深拷贝实现
* 1. 基本功能实现,解决嵌套对象问题
* 2. 对数组类型的处理
* 3. 对函数类型的处理
* 4. 对 Symbol类型的处理(分别作为键值时的处理)
* 5. 对 Set 类型的处理
* 6. 对 Map 类型的处理
* 7. 解决循环引用问题
*/
// *判断是否为对象
function isObject(value) {
const valueType = typeof value
return value !== null && (valueType === 'object' || valueType == 'function')
}
function deepClone(originValue, map = new WeakMap()) {
//TODO 判断 originValue 是否为 Set 类型
if (originValue instanceof Set) {
return new Set([...originValue])
}
//TODO 判断 originValue 是否为 Map 类型
if (originValue instanceof Map) {
return new Map([...originValue])
}
//TODO 判断 originVlaue 是否为 symbol 类型
if (typeof originValue === 'symbol') {
return Symbol(originValue.description)
}
//TODO 判断 originValue 是否为函数类型
if (typeof originValue === 'function') {
return originValue
}
//TODO 判断 originValue 是否为对象类型
if (!isObject(originValue)) {
return originValue
}
// 2. 判断 newObj 是否已存在
if (map.has(originValue)) {
return map.get(originValue)
}
//TODO 判断 originValue 是否为数组类型
const newObj = Array.isArray(originValue) ? [] : {}
// 1. 将 newObj保存下来,用于下次运行时判断是否已经创建了 newObj
map.set(originValue, newObj)
for (const key in originValue) {
newObj[key] = deepClone(originValue[key], map)
}
//TODO 对 symbol类型的 key做特殊处理
const symbolKeys = Object.getOwnPropertySymbols(originValue)
for (const skey of symbolKeys) {
newObj[skey] = deepClone(originValue[skey], map)
}
return newObj
}
// 测试代码
let s1 = Symbol('aaa')
let s2 = Symbol('bbb')
const obj = {
name: 'CoderBin',
friend: {
name: 'Jack'
},
b: undefined,
s2: s2,
// 待优化
skill: ['js', 'vue', 'react'],
foo: () => { },
[s1]: 's11',
set: new Set(['a', 'b', 'c']),
map: new Map([
['a', 'aa'],
['b', 'bb']
])
}
obj.info = obj
const newObj = deepClone(obj)
console.log(newObj)
每文一句:知识是智慧的火炬。
本次的分享就到这里,如果本章内容对你有所帮助的话欢迎点赞+收藏。文章有不对的地方欢迎指出,有任何疑问都可以在评论区留言。希望大家都能够有所收获,大家一起探讨、进步!
本文正在参加「金石计划 . 瓜分6万现金大奖」