当前位置: 首页 > Web前端 > HTML

前端手写代码系列文章(一):手写深度克隆方法

时间:2023-03-28 11:01:53 HTML

大家好,我是万天。手写代码是前端面试必不可少的一环,常见的手写代码题基本都能一一列举。网上也有很多面谈手写代码的文章,但大多不够全面和深入,一些低质量的文章甚至会误导初学者。因此,拟推出系列文章【前端手写代码】,深入讲解前端面试常见的手写代码题,并通过手写代码题,延伸讲解后面面试官想考察的基本能力.为了保证尽可能的全面和深入,本系列文章会参考其他优秀文章,参考文章会附在参考资料中。让我们从今天的深度克隆开始。深度克隆是前端面试中非常常见的手写代码题。相信有一定前端面试经验的前端同学都手写过,但是在继续看下文之前,请问自己这三个问题Question:你真的了解深度克隆吗?在面试官眼中,什么样的深度克隆方法才算合格?如何编写更好的深度克隆方法?理解深克隆和浅克隆要理解深克隆和浅克隆的区别,你需要理解JavaScript中基本类型和引用类型的区别。知识点一:JavaScript数据类型。JavaScript中有两种不同的数据类型:原始类型和引用类型。基本类型有Number、Boolean、String、Symbol、Null和Undefined;引用类型有Object、Array、Function等。基本类型按值传递,引用类型按引用传递。知识点2:按值传递和按引用传递按值传递是指每次给一个变量赋值时,都会将该值的一个副本赋给该变量;下面的例子可以形象地说明传值的过程。让a=1;让b=a;b=b+2;console.log(a);//1console.log(b);//3将a赋值给b会将a的值的副本赋值给b。当b被重新赋值时,它对a没有影响。所以最后a!==b。引用传递(Passingbyreference),传递的只是对象的引用,而不是对象的副本。引用相同的两个对象,修改任何一个对象都会影响另一个。如下,x是Array类型的数组,Array类型是引用类型,Array类型本质上是Object类型的子类型,遵循引用传递。将变量x分配给变量y只是将对x的引用传递给y。此时,x和y对同一个数组有共同的引用。constx=[1];typeofx;//objectxinstanceof数组;//truexinstanceof对象;//trueArray.prototype.__proto__===Object.prototype;//true,Array的原型指向Object的原型,如下例,修改x和y的任何一个变量都会影响另一个,因为x和y引用了同一个数组:letx=[1];让y=x;y.push(2);console.log(x);//[1,2]console.log(y);//[1,2]知识点3:深克隆和浅克隆的区别。深克隆与浅克隆的区别在于,浅克隆在复制引用类型对象时,将相同的引用传递给新的克隆对象;当深度克隆复制引用类型对象时,它会将引用类型值的副本传递给新的克隆对象。因为浅克隆不需要复制对象类型的值,所以比深克隆性能更好。两张图解释浅克隆和深克隆的区别:浅克隆:深克隆:了解了深克隆和浅克隆的概念和区别之后,我们现在正式写代码实现深克隆。乞丐版实现深度克隆最简单的方法是使用JSON.parse(JSON.stringify(target)),但是这种方法有明显的缺陷,比如:函数不能被克隆;无法克隆具有无限循环的对象;ETC;JSON.parse(JSON.stringify(目标));因此,我们需要手动实现一个深度克隆的方法。基本实现首先我们实现一个浅克隆,通过遍历克隆目标对象。functionshadowClone(target){让cloneTarget={};for(constkeyintarget){cloneTarget[key]=target[key];}返回cloneTarget;};如果我们要实现深克隆,需要在上述实现的基础上增加新的两个逻辑:如果是基本类型,可以直接返回基本类型的值;如果是引用类型,需要递归支持clone方法返回一个新的拷贝对象;functiondeepClone(target){if(typeoftarget!=='object'){返回目标;}constcloneTarget={};for(目标中的常量键){cloneTarget[key]=deepClone(target[key]);}返回克隆目标;}上面的实现,在面对对象类型的时候使用引用类型,基本可以满足要求。但是如果是数组类型呢?上面的实现还将数组转换为对象。因此,我们接下来考虑兼容阵列。考虑数组我们在考虑数组的时候,会涉及到一个新的知识点,如何判断数组类型。知识点4:数组类型的判断。判断JavaScript的类型有几种方法:Array.isArray()constarr=[1,2,3,4,5];Array.isArray(arr);//trueconstobj={id:1,name:"Josh"};Array.isArray(obj);//falseinstanceofconstarr=[1,2,3,4,5];datainstanceofArray;//trueconstobj={id:1,name:"Josh"};数组的数据实例;//falseObject.prototype.toString.callconstdata=[1,2,3,4,5];Object.prototype.toString.call(data)==='[objectArray]';//trueconstdata={id:1,name:“Josh”};Object.prototype.toString.call(data)==='[objectArray]';//false这里可能有些初学者会用typeof来判断类型,但是typeof无法判断数组类型。知识点5:typeof。typeof的返回类型如下:stringnumberundefinedobjectfunctionbooleansymbolbigint数组typeof的返回值也是object。类型[];//object接下来我们考虑代码实现中数组类型的情况。functiondeepClone(target){if(typeoftarget!=='object'){返回目标;}constcloneTarget=Array.isArray(target)?[]:{};for(letkeyintarget){cloneTarget[key]=deepClone(target[key]);}返回克隆目标;}考虑循环引用如果在对象具有循环引用时使用上述实现会怎样?constobj={a:{b:1},c:'你好'}obj.a.c=obj;cloneDeep(obj);执行上面的代码,我们会得到如下结果。因为obj中存在循环引用,所以对cloneDeep的调用会无限循环,最终导致内存溢出。为了解决以上问题,我们需要一个存储空间来存储当前对象和复制对象之间的关系。如果当前对象已经被复制,则直接返回存储的复制对象。如果没有被复制,则复制,复制的对象存入storage。这个存储空间需要是key-value的形式,所以我们选择Map来存储。使用Map的实现如下:functiondeepClone(target,map=newMap()){if(typeoftarget!=='object'){returntarget;}让cloneTarget=Array.isArray(target)?[]:{};如果(map.get(target)){返回map.get(target);}map.set(target,cloneTarget);for(constkeyintarget){cloneTarget[key]=deepClone(target[key],map);}返回克隆目标;};重新复制上面循环引用的对象obj,得到如下结果:循环引用的对象可以正常深度克隆。熟悉ES6的同学一定想到了WeakMap,Map的孪生姐妹。在这里,我们可以继续使用Wea??kMap优化我们的实现。知识点6:Map和WeakMap的区别WeakMap是弱引用的键值对集合,键值必须是对象,值可以是任意类型。与Map的区别在于Map是强引用,而WeakMap是弱引用。与Map对象不同,WeakMap键不可枚举。没有提供列出其键的方法的弱引用是什么?弱引用意味着如果没有其他引用,弱引用对象可以被垃圾回收。如果一个对象只有弱引用,那么在下一次垃圾回收机制执行时,该对象将被回收。当我们在深克隆一个大对象时,使用Map会造成很大的性能损耗,必须手动清除Map的key值来释放内存。WeakMap不存在这个问题。考虑到上面原型链的实现,我们用来获取对象key的方法是for...in,但是for...in有个问题,它会获取原型链上的所有可枚举属性,除了象征。对于深度克隆,只需要克隆对象本身的属性,不需要克隆原型链上的非自身属性。那么如何获取对象本身的属性呢?知识点7:如何获取对象自身的属性获取对象自身的属性有以下几种方式:Object.getOwnPropertyNames()获取对象自身的所有属性,包括不可枚举的属性;Object.keys()获取对象的自枚举属性;for...in+Object.hasOwnProperty()+Object.getOwnPropertySymbols()for...in以任何顺序迭代除Symbol之外的对象的可枚举属性,包括继承的可枚举属性;Object.hasOwnProperty()判断属性是否为对象本身的属性;Object.getOwnPropertySymbols()获取对象的Symbol类型的属性;可见,如果我们的目标是获取对象本身的所有属性,Object.getOwnPropertyNames()是一个合适的方法。基于以上分析,我们继续优化我们的实现。functioncloneDeep(target,map=newWeakMap()){if(typeoftarget!=='object'){returntarget;}if(map.has(target)){returnmap.get(target);}constcloneTarget=Array.isArray(target)?[]:{};map.set(target,cloneTarget);Object.getOwnPropertyNames(target).forEach((key)=>{cloneTarget[key]=cloneDeep(target[key],map);});返回克隆目标;}constobj={a:{value:1},b:{value:2}}Object.defineProperty(obj,'a',{enumerable:true});Object.defineProperty(obj,'b',{enumerable:错误的});constnewObj=cloneDeep(obj);控制台日志(newObj);//{a:{value:1},b:{value:2}}othermore多种类型在上面的实现中,我们只考虑了普通对象和数组作为引用类型。其实引用类型远不止这两种,String,Number,Boolean,RegExp,Date,Map,Set,Error等等,我们如何获取真正的引用类型呢?通过Object.prototype.toString方法,我们可以观察到各种对象实例的toString都会按照相同的格式输出。知识点8:获取引用对象真实类型的方法因此可以使用如下方法:functiongetType(obj){returnObject.prototype.toString.call(obj).slice(8,-1);}其中Symbol类型需要特别注意:知识点9:Symbol类型symbol的创建方法是一种基本数据类型(primitivedatatype)。Symbol()函数返回一个symbol类型的值,它具有静态属性和静态方法。它的静态属性公开了几个内置的成员对象;它的静态方法公开了全局符号注册表并且类似于内置对象类,但它作为构造函数并不完整,因为它不支持语法:“newSymbol()”。Symbol类型的数据可以创建如下:constsym=Symbol(1);//符号(1)typeofsym;//'symbol'不能通过newSymbol()创建Symbol类型,因为Symbol不是构造函数。如果真的要创建一个Symbol对象,可以使用下面的方法:constsym=Object(Symbol(1));//符号{Symbol(1),description:'1'}typeofsym;//'object'Object.prototype.toString.call(sym);//'[objectSymbol]'接下来,让我们支持Map、Set、RegExp、Number、String、Boolean、Date、Error、Null和Symbol对象类型。Symbol对象类型是指Object(Symbol())创建的Symbol类型。最终实现基于以上优化,我们最终实现了一种尽可能兼容各种情况的深度克隆方法,并通过测试验证了函数的正确性:functioncloneDeep(target,map=newWeakMap()){if(typeoftarget!=='object'){returntarget;}if(map.has(target)){returnmap.get(target);}consttype=Object.prototype.toString.call(target).slice(8,-1);让克隆目标;开关(类型){case'Object':case'Array':cloneTarget=type==='Array'?[]:{};map.set(target,cloneTarget);Object.getOwnPropertyNames(target).forEach(key=>{cloneTarget[key]=cloneDeep(target[key],map);});休息;case'Map':cloneTarget=newMap();map.set(target,cloneTarget);target.forEach((value,key)=>{cloneTarget.set(cloneDeep(key,map),cloneDeep(value,map));});休息;case'Set':cloneTarget=newSet();map.set(目标等,克隆目标);target.forEach((value)=>{cloneTarget.add(cloneDeep(value,map));});休息;case'RegExp':case'Number':case'String':case'Boolean':case'Date':case'Error':cloneTarget=newtarget.constructor(target);休息;case'Symbol':cloneTarget=Object(Object.prototype.valueOf.call(target));休息;case'Null':cloneTarget=null;休息;}returncloneTarget;}//testconstmap=newMap();map.set('key','value');constset=newSet();set.add('value1');set.add('value2');constobj={field1:1,field2:undefined,field3:{child:'child'},field4:[2,4,8],empty:null,map,set,bool:newBoolean(true),num:newNumber(2),str:newString(2),symbol:Object(Symbol(1)),date:newDate(),reg:/\d+/,错误:新的错误(),func1:()=>{控制台。日志('你好朋友!');},func2:函数(a,b){返回a+b;}};安慰。log(obj);constcopy=cloneDeep(obj);console.log(copy);知识点回顾在上一篇文章中,我们通过深度克隆方式的不断优化,扩展了以下知识点的学习:JavaScript数据类型取值transfer&objecttransfer深克隆和浅克隆的区别数组类型的判断typeofMap和WeakMap的区别如何获取对象自身的属性如何获取引用对象的真实类型Symbol类型创建方法总结通过以上,我们继续优化深度克隆的方法,逐步支持所有基础类型(number/boolean/string/undefined/bigint/function/symbol),引用类型(Object/Array/Map/Set/Symbol/Error/Regex/Number/String/Boolean/Date/Null)支持,并考虑到原型链、循环引用等兼容性问题,请参考《前端手写代码系列文章》。更多内容请访问万天个人主页。ReferencesPassbyValueandPassbyReferenceinJavascriptJavaScript中ValuesandReferences的区别检查一个变量是Object还是Array的类型?WeakMap在JavaScript中写一个更好的DeepClone函数对象的深度克隆JavaScript中的ShallowCopyvsDeepCopyCreatecloneDeep-BFTJavascript经典访谈DeepCopyVSShallowCopy一个类的浅拷贝和深拷贝的区别

猜你喜欢