javaScript-深浅拷贝

2021-07-26·8min
type
Post
summary
status
Published
category
tags
slug
date
Jul 26, 2021
password
icon
📌
在实际的开发工作中,有些场景往往需要我们对已有对象进行再加工。
但在原对象上直接修改是不可取的,这会造成不可逆的数据篡改。
这时候就需要有合适的方法,对原对象进行拷贝(复制),然后再修改拷贝对象上的值。

浅拷贝

浅拷贝复制一份原对象上的数据到新对象。 如果原对象上的是基本类型的数据,则复制值本身到新对象。 如果原对象上的是引用类型的数据,则复制引用地址到新对象。
简单来说,浅拷贝只拷贝一层,也就是原对象与新对象本身并不相等,修改第一层的数据并不会影响对方。
但如果原对象是多层级的,那修改新对象中的子对象,原对象中的子对象,也会发生改变。

JS中的浅拷贝方法

1.Array.slice
slice: 从原数组切分出新数组;
slice方法接收两个可选的参数(begin, end),并根据这两个参数返回一个新的数组。
可以看到,修改b\[0] = 100时,a对象并不会产生变化,但当修改b\[3].c = 6时,a对象也发生了改变。这就是浅拷贝,只拷贝一层。
2.Array.concat
concat: 合并数组
concat方法接受任意多个值,这些值可以是具体的值,也可以是数组对象。并返回一个合并后的新数组
concat方法的代码示例和slice方法差不多,这里就不重复举例了。
3.Object.assign
assign: 将所有源对象的可枚举属性值分配到目标对象。
assign方法接收一个目标对象以及任意多个源对象,并返回修改后的目标对象。如果键名重复,则会进行覆盖。
只拷贝了一层,浅拷贝无疑。
除了这三种,还有使用展开运算符也是浅拷贝,这里就不赘述了。
下面来手动实现一下浅拷贝。

手动实现浅拷贝

浅拷贝的实现相对来说比较简单,就是遍历原对象上的属性,并复制一份到新对象上。
但要注意几点:
  1. 对参数进行校验
  1. 兼容对象和数组两种情况

深拷贝

深拷贝精确复制原对象上的数据到新对象 无论是基础数据类型还是引用数据类型。都是复制值本身。
深拷贝相对于浅拷贝来说,拷贝的层级是无限深的。
无论怎么修改深拷贝后的对象,都不会影响到原对象。

现有的深拷贝方法

JSON.parse(JSON.stringify(source))
可以发现,修改新对象的任何层级的值,都不会影响到原对象,这就实现了深拷贝。
但是,这个方法有几个问题:
  1. undefined、symbol、函数会被忽略,或者被替换为null(数组对象中)
  1. 对于循环引用的对象,会抛出错误。
  1. 无法处理正则对象以及Date对象
如果开发中不需要考虑以上的情况,直接用这个方法是极好的。
此外,jQuery.extend() 和 lodash.cloneDeep()也可以实现深拷贝。
下面,让我们自己实现一个深拷贝。

手动实现深拷贝

在实现深拷贝之前,再来思考下深拷贝比浅拷贝多了什么特点?
没错,就是无限层级的拷贝。
那么在浅拷贝的基础上,加上递归,是不是就能实现深拷贝了?
动手试试。
测试一下:
可以看到,深层次的对象已经不再相等,证明已经完成了深层次的拷贝。
但这个深拷贝的实现还有问题,将上面的测试代码复制到浏览器的控制台查看运行结果。
可以发现:
  1. symbol丢失。
  1. Date对象和正则对象都变成了空的对象。
这个结果显然不是我们想要的。接下来一一解决。

symbol丢失

首先要思考的是,为什么symbol会丢失?是哪一步引起了symbol的丢失?
这里直接给出答案,原因是我们使用了for...in。
MDN上对for...in的定义 for...in语句以任意顺序遍历一个对象的除Symbol以外的可枚举属性。包括原型上的属性。
那么为了能够拷贝symbol,我们就不能使用for...in语句了,我们需要能遍历出symbol属性的方法。
方法一:Object.getOwnPropertySymbols(...)
Object.getOwnPropertySymbols方法返回一个由给定对象自身的symbol属性组成的数组,如果没有,则返回一个空数组。
方法二:Reflect.ownKeys(...)
Reflect.ownKeys()返回一个由目标对象自身的属性键组成的数组。无论该属性是否可被枚举。
这里用方法一来作示例,感兴趣的同学可以自行思考一下方法二如何实现。
方法一的思路就是先获取原对象中的所有symbol,先处理symbol,处理完再处理其他的属性。
代码如下:
再用刚才的测试代码测试一遍,完美解决Symbol丢失的问题。

处理Date对象以及正则对象

其实这个问题解决起来也很简单,就是我们的代码中没有对Date对象和正则对象进行判断和处理。
首先封装一个判断具体类型的方法
然后对正则和Date对象进行相应的处理,代码改动如下:
再用刚才的测试代码测试一遍,完美解决Date对象以及正则对象为空的问题。
对于日常的开发来说,这里的深拷贝代码已经完全够用了。
但这个实现还有待改进的问题,感兴趣的同学可以继续阅读。

循环引用与相同引用

思考一下,有一种情况,一个对象,它的子对象引用了它本身。
在控制台中打印这个对象,会发现它是一个无限嵌套的对象。
这时候用我们手写的深拷贝来处理这个对象,会发生什么呢?
果然,栈爆了。
另一种情况,一个对象中,它的两个属性指向同一个对象。
但用我们手写的深拷贝来处理后发现两个属性不再指向同一个对象。
那么结果显然也是不对的。
那我们该如何处理这种循环引用以及相同引用的对象呢?
想一想,对于已经处理过的对象,将它存起来,再遇到时,直接返回已经处理过的对象,是不是就可以解决问题了?
动手试试:
测试一下:
完美解决循环引用以及相同引用的问题。

还存在的问题

目前手写的深拷贝方法还有问题。
由于使用的是递归,如果对象的嵌套层级过深,那么就会引起爆栈。
关于爆栈的解决方法可以参考:
这里便不再赘述了,主要是我也还没弄懂呐。

参考文章

> cd ..