手撕JavaScript深拷贝&浅拷贝
深拷贝要考虑到所有数据类型
1 |
|
- 7基本数据类型:string、number、boolean、null、undefined、symbol、BigInt
- symbol类型做value、symbol类型做key
- 引用类型:数组、正则、日期、Error、Set、Map
- 函数类型
- 深层次对象类型
- 本文没有考虑
ArrayBuffer
这种引用数据类型;lodash的深拷贝中会处理
使用展开运算符完成对象拷贝存在的问题
- 展开运算符可以把obj的每一项展开然后赋值给obj2
- 因为obj2在堆内存中开辟了一份新的内存空间来存储从obj拷贝过来的所有字段的值
- obj2 === obj => false,因为是一个新的对象,所以不相等
- obj2中的child和arr又是引用类型,内部还包含其他基本数据类型或者引用类型
- obj2.child === obj.child => false
- …展开运算符是浅拷贝,只拷贝了obj的第一级的key对应的值给到obj2,第二级以及后面的第三级、第四级没有进行拷贝;所以obj.child 和 obj2.child指向的是同一块内存
使用JSON.stringfy进行深拷贝存在的问题
- BigInt类型的值不能被序列化
- 把BigInt类型的值先去掉,就能使用JSON.stringfy来深拷贝
- undefined类型的值丢了
- 函数类型的值丢了
- Symbol类型的值丢了
- 正则类型的值变成空对象了
- Error类型的值变成空对象了
- Set类型的值变成空对象了
- Map类型的值变成空对象了
- key是Symbol类型的话,该key都丢了,即使自己for in循序也不能遍历Symbol类型的key
- Date日期类型的值变成字符串了
JSON.stringify会把值变成字符串,对于特殊的数据类型就是转不了字符串在反序列化回来,所以就会存在问题
- JSON.parse(JSON.stringify(obj))先stringify再parse得到的对象确实是深拷贝
- 第二级child也进行深拷贝了
第一版深拷贝实现
1 |
|
基本思路
- 如果是基本数据类型直接把原始的值赋给新的克隆后的对象相应的key
- 如果是引用类型的值,就new一个新的该引用类型对象,然后值赋给新的克隆后的对象相应的key
存在的问题
- 正则表达式的内容丢失
- error对象的message丢失
- set对象长度为0
- map对象长度为0
- function函数类型的值因为typeof返回的是’function’并不是’object’,所以直接返回了
第二版深拷贝实现
1 |
|
- 正则类型的值只需要把原本的正则target传给新的构造函数就不会丢失,而且还创一个新的实例
- Error类型需要把message传给构造函数
- Funtion类型需要Object.prototype.toString.call(target).slice(8, -1).toLowerCase()来拿到类型的相信信息,从而对函数类型的值单独处理,不能直接返回
第三版深拷贝实现-循环依赖
1 |
|
在控制台可以无限查看obj,这就是循环依赖
如果沿用原来的代码会报调用次数太多,爆栈 Maximum call stack size exceeded
解决思路
- 避免对象或者数组类型的值循环递归
方案一
- 利用缓存,如果某个对象或者数组类型的值已经处理过就直接返回,然后就不会进入无限递归了
- 采用WeakMap,是因为弱引用,使用完就被垃圾回收了,避免递归内存溢出
1 |
|
方案二
- 利用WeakSet或数组来存放某个对象或者数组类型的target,发现处理过就直接返回
- 直接返回
1 |
|
- 直接返回就相当于没有拷贝
- 返回target,相当于没拷贝
优化传递map
1 |
|
同时支持浅拷贝和深拷贝
1 |
|
函数类型的深拷贝
- 函数拷贝有两种方案
- 方案一返回一个代理函数,函数内部还是调用原函数,就是本文用的方案
- 方案二new Function,方案如下
1 |
|
手撕JavaScript深拷贝&浅拷贝
https://retech-fe.github.io/blog/2023/03/01/deepclone/