扩展 Object.assign 实现深拷贝
本文参考: Object.assign 原理及其实现
需求场景
上一篇文章:手写实现深拷贝中,我们讲了浅拷贝和深拷贝,也实现了深拷贝方案。
但深拷贝,它是基于一个原对象,完完整整拷贝一份新对象出来,假如我们的需求是要将原对象上的属性完完整整拷贝到另外一个已存在的对象上,这时候深拷贝就有点无能为力了。
就有点类似于 Object.assign():
1 | var a = { |
将一个原对象上的属性拷贝到另一个目标对象上,最终结果取两个对象的并集,如果有冲突的属性,则以原对象上属性为主,表现上就是直接覆盖过去,这是 Object.assign() 方法的用途。
但很可惜的是,Object.assign 只是浅拷贝,它只处理第一层属性,如果属性是基本类型,则值拷贝,如果是对象类型,则引用拷贝,如果有冲突,则整个覆盖过去。
这往往不符合我们的需求场景,讲个实际中常接触的场景:
在一些表单操作页面,页面初始化时可能会先前端本地创建一个对象来存储表单项,对象中可能会有一些初始值,然后访问了后台接口,读取当前页的表单数据,后台返回了 json 对象,这时候我们希望当前页的表单存储对象应该是后台返回的 json 对象和初始创建的对象的并集,有冲突以后台返回的为主,如:
1 | var a = { |
其实,说白了,这种需求就是希望可以进行深拷贝,而且是深拷贝到一个目标对象上。
上一篇的深拷贝方案虽然可以实现深度拷贝,但却不支持拷贝到一个目标对象上,而 Object.assign 虽然支持拷贝到目标对象上,但它只是浅拷贝,只处理第一层属性的拷贝。所以,两种方案都不适用于该场景。
但两种方案结合一下,其实也就是该需求的实现方案了,所以要么扩展深拷贝方案,增加与目标对象属性的交集处理和冲突处理;要么扩展 Object.assign,让它支持深拷贝。
实现方案
本篇就选择基于 Object.assign,扩展支持深拷贝:assignDeep。
这里同样会给出几个方案,因为深拷贝的实现可以用递归,也可以用循环,递归比较好写、易懂,但有栈溢出问题;循环比较难写,但没有栈溢出问题。
递归版
1 | function assignDeep(target, ...sources) { |
要注意的地方,其实也就是模拟实现 Object.assign 的一些细节处理,比如参数校验,参数处理,属性遍历,以及引用关系丢失问题。
循环版
1 | function assignDeep(target, ...sources) { |
测试用例:
1 | var a = {}; |
上面的方案仍旧不是100%完美,仍旧存在一些不足:
- 没有考虑 ES6 的 set,Map 等新的数据结构类型
- get,set 存取器逻辑无法拷贝
- 没有考虑属性值是内置对象的场景,比如 /sfds/ 正则,或 new Date() 日期这些类型的数据
- 为了解决循环引用和引用关系丢失问题而加入的 hash 缓存无法识别一些属性冲突场景,导致同时存在冲突和循环引用时,拷贝的结果可能有误
- 等等未发现的逻辑问题坑
虽然有一些小问题,但基本适用于大多数场景了,出问题时再想办法慢慢填坑,目前这样足够使用了,而且,当目标对象是空对象时,此时也可以当做深拷贝来使用。
当然,也欢迎指点一下。
TypeScript 业务版
根据实际项目中的业务需求,进行的相关处理,就没必要像上面的通用版考虑那么多细节,比如我项目中使用 ts 开发,业务需求是要解决实体类数据的初始化和服务端返回的实体类的交集合并场景。
另外,只有对象类型的属性需要进行交集处理,其余类型均直接覆盖即可:
1 | /** |
因为直接基于业务需求场景来进行的封装,所以我很明确参数的结构是什么,使用的场景是什么,很多细节就没处理了,比如参数的校验等。
而且,这个目的在于解决初始化问题,所以并不是一个深克隆,而是直接在原对象上进行操作,等效于将初始化的值都复制到原对象上,如果原对象同属性没有值的时候。