拷贝的含义所谓拷贝就是在不改变原始数据的情况下克隆数据,操作数据。有的文章或者面试官提到了copy函数和copy类,纯属白搭。复制的函数与原始函数相同。如果要扩展功能,为什么不使用原来的功能,用新的功能封装呢。如果要扩展类,使用继承和复制功能是完全没有意义的操作。拷贝有两种类型,浅拷贝和深拷贝。浅拷贝只会扩展复制对象的第一层。如果数据包含引用类型,克隆的对象仍然指向原始对象,修改克隆对象可能会影响原始对象。一般浅拷贝推荐使用...扩展运算符,快速方便。constarr=[1,2,3]constarrClone=[...arr]constobj={a:1,b:{c:2,},}constobjClone={...obj,}objClone.a=2objClone.b.c=3console.log(obj.a)//1console.log(obj.b.c)//之前的浅拷贝发现了3个深拷贝问题是,多层引用对象拷贝后,克隆对象被修改时,原始对象的数据也可能发生变化,这显然不是我们想要的。深拷贝就是为了解决这个问题。对于多层数据,最常见的深拷贝是使用JSON转换:JSON.parse(JSON.stringify(obj)),但是JSON转换有很多缺点。JSON只能转换普通对象。还有数组,JS中很多类对象不支持,比如:Map、Set、Date、RegExp等。JSON在一些基本类型的转换上也有问题,比如:将NaN转换为null,忽略Symbol,BigInt报错JSONcannothandleloops引用的问题constobj={}obj.obj=objJSON.stringify(obj)//TypeError:ConvertingcircularstructuretoJSON综上所述,下一章我们将实现自己的深拷贝功能。重新解释/***@description:深拷贝功能*@param{any}需要拷贝的值数据*@param{Map}[stack]记录拷贝的对象,避免循环引用*@return{any}拷贝完成的数据*/functiondeepClone(value,stack){constobjectTag='[objectObject]'constsetTag='[objectSet]'constmapTag='[objectMap]'constarrayTag='[objectArray]'//获取对象类tagconsttag=Object.prototype.toString.call(value)//只有需要递归深拷贝的类型包括objects,arrays,collections,maps//其他的都直接返回constneedCloneTag=[objectTag,arrayTag,setTag,mapTag]if(!needCloneTag.includes(tag)){returnvalue}//无法获取代理对象的属性名,只能返回if(valueinstanceofProxy){returnvalue}//返回结果继承原型让结果如果(tag==arrayTag){//由于不会遍历Array的空属性,单纯继承原型会导致不同的长度result=newvalue['__proto__'].constructor(value.length)}else{result=newvalue['__proto__'].constructor()}//记录复制的对象//用于解决循环引用栈问题||(stack=newMap())if(stack.has(value)){returnstack.get(value)}stack.set(value,result)//递归复制mapif(tag==mapTag){for(const[key,item]ofvalue){result.set(key,deepClone(item,stack))}}//递归复制集合if(tag==setTag){for(constitemofvalue){result.add(deepClone(item,stack))}}//递归复制对象/数组的属性for(constpropofObject.keys(value)){result[prop]=deepClone(value,stack)}//复制符号属性for(constsyofObject.getOwnPropertySymbols(value)){result[sy]=deepClone(value,stack)}returnresult}说明在上面的代码中,我们根据传入数据的类标签来区分数据类型。class标签的相关内容可以参考JS中各个数据类型的检测和转换的详细说明或者递归深拷贝Objects的符号类型用法介绍,这里我先说明一下:我们只用递归深拷贝具有数据的对象:对象、数组、集合和映射。对于基本数据类型,不能直接存储和返回数据。对于Date、RegExp、Function、Number、String等对象,由于它们的属性是不可变的,使用原始对象和克隆对象是一样的,不需要复制,也直接返回。对于不可遍历的对象或属性,如:弱引用对象(WeakMapWeakSet)、代理对象(Proxy)、使用Object.defineProperty定义的不可迭代属性,由于无法获取它们的key/property,因此无法复制。还有一些类似数组的对象也可以存储数据(TypedArrays,ArrayBuffer,arguments,nodeList),平时用的不多,复制方式和数组类似,在中没有体现为简单起见的代码。下一步,调用对象原型的构造函数,获取新的实例,也继承原型。由于复制的数组必须与原数组长度相同,所以在调用数组(或其子类)的构造函数时必须传入长度。然后用一个Map记录原始对象中复制的对象,避免了循环引用无限递归的问题。最后根据对象的类型,递归复制其属性值,尤其是Map和Set。对象和数组可以通过Object.keys()获取所有键/索引,再次复制交易品种属性,结束深拷贝复制代码。总结一下自己实现的深拷贝功能,与JSON转换相比,有如下优势。它可以处理Map和Set等数据类型。可以继承原型的属性,解决循环引用问题。虽然我们的深拷贝代码可以拷贝类的实例,但是对于Constructors会产生副作用,并且可能会出现错误。下面是我在项目中遇到的一个Bug。idthis.itemMap=newMap()}newItem(item){this.itemMap.set(++this.itemId,item)returnthis.itemId}}classItem{constructor(){//每个新的Item都有获取来自全局项目的ID并将其添加到itemMapthis.itemId=globalData.project.newItem(this)}}constproject=newProject()globalData.project=projectconstitem=newItem()console.log(globalData.project)//Project{//itemId:1//itemMap:Map(1){1=>Item}//}constclone=deepClone(project)//无限创建Item,页面卡住及探索原因是因为for的遍历使用itemMap的时候,会创建一个新的Item添加到itemMap中,新的Item再次迭代,导致无限的创建和添加Item。还有一种方案,就是先把要遍历的属性保存在数组中,只遍历数组//递归复制映射if(tag==mapTag){for(constkeyof[...value.keys()]){result.set(key,deepClone(value.get(key),stack))}}//递归复制集合if(tag==setTag){for(constitemof[...value.values()]){result.add(deepClone(item,stack))}}但是这样不一定能满足我们想要的结果,比如由于我们不希望将新克隆的对象添加到itemMap中,所以在我的项目中,我为构造函数有副作用的类定义了自己的clone方法来专门实现复制功能。结语目前还没有一个深拷贝功能可以完美的满足所有需求,本文给出一个比较通用的深拷贝功能,希望读者能够理解和掌握,在需要的时候自定义自己的拷贝功能。文中如有不明白或不严谨的地方,欢迎评论提问。如果喜欢或者觉得有帮助,希望大家能够点赞关注,为作者打气。
