当前位置: 首页 > 科技观察

超详细的JavaScript深浅拷贝实现!

时间:2023-03-21 01:59:55 科技观察

1.浅拷贝浅拷贝是指一个新对象精确复制原对象的属性值。如果副本是基本数据类型,则复制基本数据类型的值;如果副本是引用数据类型,则副本是内存地址。如果其中一个对象的引用内存地址发生变化,另一个对象也会发生变化。1.Object.assign()object.assign是ES6中object的一个方法,可以用来合并JS对象。我们可以用它来实现浅拷贝。该方法的参数target指的是目标对象,sources指的是源对象。用法形式如下:Object.assign(target,...sources)用法举例:lettarget={a:1};letobject2={b:{d:2}};letobject3={c:3};对象.assign(目标,object2,object3);控制台日志(目标);这里通过Object.assign将object2和object3复制到目标对象中,我们尝试将object2对象的b属性中的d属性从2改为666:object2.b.d=666;控制台日志(目标);可以看出target的b属性值的d属性值变成了666,因为b属性值是一个对象,节省了对象Address的内存,当原来的对象发生变化时,引用它的值也会改变。注意:如果目标对象和源对象有同名属性,或者多个源对象有同名属性,后面的属性会覆盖前面的属性;如果函数只有一个参数,当参数为对象时,直接返回对象;当参数不是对象时,会先将参数转成对象再返回;因为null和undefined不能转化为对象,所以第一个参数不能为null或undefined,否则会报错;它不会复制对象的继承属性,也不会复制对象的不可枚举属性,可以复制Symbol类型的属性。实际上,Object.assign会循环遍历原始对象的可枚举属性,通过复制的方式赋值给目标对象的相应属性。2、展开运算符在构造文字对象时使用展开运算符来复制属性。使用形式如下:letcloneObj={...obj};使用示例:letobj1={a:1,b:{c:1}}letobj2={...obj1};obj1.a=2;console.log(obj1);控制台日志(obj2);obj1.b.c=2;console.log(obj1);控制台日志(obj2);扩展运算符的作用类似于object.assign实现的浅拷贝,如果属性两者都是基本类型的值,使用扩展运算符进行浅拷贝会更方便。3、数组浅拷贝(1)Array.prototype.slice()slice()方法是JavaScript数组方法,可以在不改变原数组的情况下,从现有数组中返回选中的元素。使用方法如下:array.slice(start,end)该方法有两个参数,都是可选的:start:指定从哪里开始选择。如果为负,则它指定从数组末尾开始的位置。也就是说,-1指的是最后一个元素,-2指的是倒数第二个元素,依此类推。end:指定结束选择的位置。该参数为数组切片末尾的数组下标。如果不指定该参数,拆分后的数组包含数组从头到尾的所有元素。如果此参数为负数,则它指定从数组末尾开始计数的元素。如果两个参数都不写,可以实现一个数组的浅拷贝:letarr=[1,2,3,4];console.log(arr.slice());//[1,2,3,4]console.log(arr.slice()===arr);//falseslice方法不会修改原数组,只会返回一个浅拷贝原数组中元素的新数组。原始数组的元素根据以下规则复制:如果元素是对象引用(不是实际对象),slice会将对象引用复制到新数组中。两个对象引用都指向同一个对象。如果引用的对象发生变化,新数组和原数组中的这个元素也会发生变化。对于字符串、数字和布尔值,slice将值复制到一个新数组中。在其他数组中修改这些字符串或数字或布尔值不会影响其他数组。如果向任一数组添加新元素,则另一个不受影响。(2)Array.prototype.concat()concat()方法用于合并两个或多个数组。该方法不改变原来的数组,而是返回一个新数组。用法如下:arrayObject.concat(arrayX,arrayX,...,arrayX)该方法的参数arrayX为数组或值,将合并到arrayObject数组中。如果省略所有arrayX参数,concat返回调用此方法的现有数组的浅表副本:letarr=[1,2,3,4];console.log(arr.concat());//[1,2,3,4]console.log(arr.concat()===arr);//falseconcat方法创建一个新的数组,由被调用对象中的元素组成,每个参数的顺序是参数的一个元素(参数是数组)或者参数本身(参数不是数组)。它不会递归到嵌套数组参数中。concat方法不会改变此数组或作为参数提供的任何数组,而是返回一个浅表副本,其中包含与原始数组组合的相同元素的副本。原始数组的元素按如下方式复制到新数组中:对象引用(不是实际对象):concat将对象引用复制到新数组中。原始数组和新数组都引用相同的对象。也就是说,如果引用的对象被修改,则更改对新数组和原始数组都是可见的。这包括也是数组的数组参数的元素。字符串、数字和布尔值等数据类型:concat将字符串和数字的值复制到一个新数组中。4.浅拷贝的手写实现基于以上对浅拷贝的理解,实现浅拷贝的思路:对基本类型进行最基本的拷贝;为引用类型开辟一个新的存储空间,复制一层对象属性。代码实现://浅拷贝的实现;functionshallowCopy(object){//只复制对象if(!object||typeofobject!=="object")return;//判断是新建数组还是对象letnewObject=Array.isArray(object)?[]:{};//遍历对象,只有判断为(letkeyinobject)才复制对象的属性{if(object.hasOwnProperty(key)){newObject[key]=object[key];}}returnnewObject;}这里使用了hasOwnProperty()方法,返回一个布尔值,表示对象自身属性中是否有指定属性。所有继承Object的对象都会继承hasOwnProperty()方法。此方法可用于检查对象是否为自己的属性。可见,所有的浅拷贝只能复制一层对象。如果存在对象的嵌套,那么浅拷贝就无能为力了。深拷贝就是为了解决这个问题而诞生的。可以解决多层对象嵌套问题,完全实现复制。2.深拷贝深拷贝是指对于简单数据类型,直接拷贝其值,而对于引用数据类型,在堆内存中开辟一块内存用于存放拷贝的对象,原始对象类型数据为复制过来。两个对象相互独立,分属于两个不同的内存地址。修改其中一个不会改变另一个。1.JSON.stringify()JSON.parse(JSON.stringify(obj))是比较常用的深拷贝方法之一,其原理是利用JSON.stringify将JavaScript对象序列化为JSON字符串),并进行转换将对象中的内容转化为字符串,然后使用JSON.parse反序列化,从字符串中生成一个新的JavaScript对象。这种方法是目前我在公司项目开发中使用最多的深拷贝方法,也是最简单的方法。用法示例:letobj1={a:0,b:{c:0}};letobj2=JSON.parse(JSON.stringify(obj1));obj1.a=1;obj1.b.c=1;console.log(obj1);//{a:1,b:{c:1}}console.log(obj2);//{a:0,b:{c:0}}这个方法简单粗暴,但是也存在使用这个方法需要注意的一些问题:复制的对象,经过JSON.stringify()处理后会消失。无法复制不可枚举的属性;无法复制对象的原型链;复制Date引用类型将成为一个字符串;复制RegExp引用类型将成为一个空对象;对象中包含NaN、Infinity和-Infinity,JSON序列化后的结果会变成null;复制对象的循环应用是不可能的,即对象形成一个环(obj[key]=obj)。在日常开发中,以上情况一般很少出现,所以这种方式基本可以满足日常开发需要。如果需要复制的对象存在上述情况,可以考虑使用以下方法。2、函数库lodash也提供了_.cloneDeep用于深拷贝,可以直接导入使用:var_=require('lodash');varobj1={a:1,b:{f:{g:1}},c:[1,2,3]};varobj2=_.cloneDeep(obj1);console.log(obj1.b.f===obj2.b.f);//falseattachlodashmiddledepthhere复制源供大家学习的代码:/***value:要复制的对象*bitmask:位掩码,其中1为深拷贝,2为复制原型链上的属性,4为复制Symbols属性*customizer:自定义clone函数*key:传入值的key*object:传入值的父对象*stack:栈,用于处理循环引用*/functionbaseClone(value,bitmask,customizer,key,object,stack){letresult//flagconstisDeep=bitmask&CLONE_DEEP_FLAG//深度复制,trueconstisFlat=bitmask&CLONE_FLAT_FLAG//复制原型链,falseconstisFull=bitmask&CLONE_SYMBOLS_FLAG//复制Symbol,true//自定义克隆functionif(customizer){结果=对象?customizer(value,key,object,stack):customizer(value)}if(result!==undefined){returnresult}//非对象if(!isObject(value)){returnvalue}constisArr=Array.isArray(value)consttag=getTag(value)if(isArr){//数组result=initCloneArray(value)if(!isDeep){returncopyArray(value,result)}}else{//对象constisFunc=typeofvalue=='function'if(isBuffer(value)){returncloneBuffer(value,isDeep)}if(tag==objectTag||tag==argsTag||(isFunc&&!object)){result=(isFlat||isFunc)?{}:initCloneObject(值)如果(!isDeep){返回isFlat?copySymbolsIn(value,copyObject(value,keysIn(value),result)):copySymbols(value,Object.assign(result,value))}}else{if(isFunc||!cloneableTags[tag]){返回对象?value:{}}result=initCloneByTag(value,tag,isDeep)}}//循环引用堆栈||(stack=newStack)常量堆栈ed=stack.get(value)if(stacked){returnstacked}stack.set(value,result)//Mapif(tag==mapTag){value.forEach((subValue,key)=>{result.set(key,baseClone(subValue,bitmask,customizer,key,value,stack))})returnresult}//设置if(tag==setTag){value.forEach((subValue)=>{result.add(baseClone(subValue,bitmask,customizer,subValue,value,stack))})returnresult}//TypedArrayif(isTypedArray(value)){returnresult}//Symbol&原型链constkeysFunc=isFull?(isFlat?getAllKeysIn:getAllKeys):(isFlat?keysIn:keys)constprops=isArr?undefined:keysFunc(value)//遍历赋值arrayEach(props||value,(subValue,key)=>{if(props){key=subValuesubValue=value[key]}assignValue(result,key,baseClone(subValue,bitmask,customizer,key,value,stack))})//returnresultreturnresult}3.深拷贝的手写实现(一)基本递归实现实现深拷贝的思路是利用forin遍历属性传入参数的值,如果值是基本类型,直接复制,如果是引用类型,递归调用函数,实现代码如下:functiondeepClone(source){//判断源是否为对象if(sourceinstanceofObject==false)returnsource;//根据源类型初始化结果变量lettarget=Array.isArray(source)?[]:{};for(letiinsource){//判断是否是自己的属性if(source.hasOwnProperty(i)){//判断数据i的类型if(typeofsource[i]==='object'){目标[i]=deepClone(源[i]);}else{目标[i]=来源[i];}}}返回目标;}console.log(clone({b:{c:{d:1}}}));//{b:{c:{d:1}}})这样实现了深拷贝,但是也存在一些问题:不可枚举的属性和Symbol类型不能拷贝;普通引用类型的值只能递归复制,不能正确复制Date、RegExp、Function等引用类型;可能存在循环引用问题(2)优化递归上面的实现只是基础版本的深拷贝。针对以上问题,可以尝试解决:使用Reflect.ownKeys()方法解决不可枚举属性和Symbol类型无法复制的问题。Reflect.ownKeys()方法返回目标对象自身属性键的数组。它的返回值相当于:Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target));当参数值为Date,RegExp类型时,直接生成新实例返回;使用Object.getOwnPropertyDescriptors()方法获取对象的所有属性和对应的属性。简单来说,此方法返回有关给定对象的所有属性的信息,包括有关getter和setter的信息。它允许创建对象的副本并在复制包括getter和setter在内的所有属性时克隆它。使用Object.create()方法创建一个新对象,该对象继承传递给原始对象的原型链。Object.create()方法创建一个新对象,使用现有对象提供新创建对象的__proto__。使用Wea??kMap类型作为哈希表。WeakMap是一种弱引用类型,可以防止内存泄漏,因此可以用来检测循环引用。如果有循环,则引用直接返回存储在WeakMap中的值。WeakMap的特点是里面存放的对象不会影响垃圾回收。如果保存在WeakMap中的节点没有被其他地方引用,即使它还在WeakMap中,也会被垃圾回收回收。在深拷贝的过程中,里面的所有引用对象都被引用了。为了解决循环引用的问题,在深拷贝的过程中,希望有一个数据结构可以记录每个引用对象是否被使用过,但是在深拷贝完成后,可以自动将数据收集垃圾以避免内存泄漏。代码实现:functiondeepClone(obj,hash=newWeakMap()){//日期对象直接返回一个新的日期对象if(objinstanceofDate){returnnewDate(obj);}//正则对象直接返回一个新的正则对象if(objinstanceofRegExp){returnnewRegExp(obj);}//如果是循环引用,用weakMap解决if(hash.has(obj)){returnhash.get(obj);}//获取对象所有属性的描述letallDesc=Object.getOwnPropertyDescriptors(obj);//遍历传入参数的所有key的属性letcloneObj=Object.create(Object.getPrototypeOf(obj),allDesc)hash.set(obj,cloneObj)for(letkeyofReflect.ownKeys(obj)){if(typeofobj[key]==='object'&&obj[key]!==null){cloneObj[key]=deepClone(obj[key],hash);}else{cloneObj[key]=obj[key];}}returncloneObj}可以使用以下数据进行测试:letobj={num:1,str:'str',boolean:true,und:undefined,nul:null,obj:{name:'object',id:1},arr:[0,1,2],func:function(){console.log('function')},date:newDate(1),reg:newRegExp('/regular/ig'),[符号('1')]:1,};Object.defineProperty(obj,'innumerable',{enumerable:false,value:'non-enumerableproperties'});obj=Object.create(obj,Object.getOwnPropertyDescriptors(obj))obj.loop=obj//将循环设置为循环引用属性letcloneObj=deepClone(obj)console.log('obj',obj)console.log('cloneObj',cloneObj)运行结果如下:可以看到,这个基本实现了大部分数据类型的深拷贝,但是还是有一些缺陷,比如这个方法不能拷贝Map和Set结构,你可以自己实现