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

深入理解JavaScript——复制

时间:2023-03-27 18:17:12 JavaScript

的秘诀上一篇文章中介绍了JavaScript数据类型分为基本类型和引用类型。在内存中,引用类型虽然比较重,但它存在于堆内存中。所以在复制基本类型的时候,直接复制即可。复制引用类型时,复制的不是对象(引用类型有且只有一种数据类型——对象),而是复制对象在内存中的地址。一切都是对象。我们曾经明确表达过一个观点,在JavaScript的世界里,除了undefined和null,一切都是对象。对象的使用中最常使用的点之一就是赋值复制,当你复制出错时,情况会很糟糕。所以我们这一节就是讲解对象中的拷贝。首先,JavaScript没有不可变的数据结构。不可变数据结构在函数式编程中是必须的。可变的优点是节省内存或使用可变性来做某事。但是在复杂的开发中,它的副作用远大于好处,所以有浅拷贝和深拷贝之分。下面是作者的一些问答。为什么是复制地址的对象?为了性能(节省内存),想象一下,如果每个对象都是一个拷贝值,那么这个对象很大/很多,占用的内存会呈几何级数增长。如何复制对象的值Object.assign扩展操作符slice(数组方法)concat(数组方法)JSON.stringify的五个方法可以复制对象的值,而前四个是浅拷贝,JSON.stringify是深拷贝。什么是浅拷贝?什么是深拷贝?浅拷贝就是创建一个新的对象。该对象复制原始对象的属性值。属性是基本类型,复制基本类型的值。修改内容不影响属性。复制:将整个对象复制到另一块内存中,修改后的内容互不影响。说白了就是浅拷贝只拷贝一层,深拷贝直接拷贝一个对象。为什么要有浅拷贝?而是直接深拷贝。当然,这个问题也被网友提出来了——JS浅拷贝的作用是什么?,虽然笔者没有找到相关资料,但怀疑是因为性能,浅拷贝可以应对很多场景,深拷贝就没必要了。该设计让开发者使用更少,无形中提升了开发体验。Object.assignObject.assign()方法可以将原始对象本身的任意数量的可枚举属性复制到目标对象,然后返回目标对象。属性的引用,而不是对象本身,是ES6中Object对象的一个??新的方法参数:target:目标对象sources:任意数量的原始对象。返回值:将返回目标对象适用对象:Object案例一:varobj1={a:10,b:20,c:30};varobj2=Object.assign({},obj1);obj2.b=100;console.log(obj1);//{a:10,b:20,c:30}console.log(obj2);//{a:10,b:100,c:30}情况2:varobj={a:{a:'hello',b:21}};varinitialObj=Object.assign({},obj);initialObj.a.a='改变';console.log(obj.a.a);//"Change"可以看出,Object只能复制第一层的对象,如果再深复制一层,就会出问题。因此,Object.assign是一个浅拷贝扩展运算符(...)扩展运算符,可以在函数调用/数组构造时,在语法层面对数组表达式或字符串进行扩展;也可以在构造字面量对象时扩展对象表达式通过key-value进行扩展当然,我们都知道扩展运算符的作用不是复制。不过也没什么错,浅拷贝也是展开算子的作用之一适用对象:对象/数组案例一:一维数组vararr=[1,2,3];vararr2=[...arr];arr2.push(4);//arr2[1,2,3,4]//arr1不受影响情况2:多维数组vara=[[1,2],[3,4],[5,6],];varb=[...a];b.shift().shift();//b[[3,4],[5,6]]//a[[2],[3,4],[5,6]]扩展运算符也是浅拷贝。sliceslice()方法返回一个新的数组对象,它是由begin和end(包括begin但不包括end)确定的原始数组的浅拷贝。原数组不会改变适用对象:数组案例:constfamily=['father','mother','brother',['sister0','sister1','sister2'],];constcopyFamily=family.slice();copyFamily[0]='father1';copyFamily[3][1]='brother1';console.log(family);//['父亲','母亲','兄弟',['sister0','brother1','sister2']]console.log(copyFamily);//['father1','mother','brother',['sister0','brother1','sister2']]//复制一层,第二层开始参照上面的情况,slice只能复制一层,第二层是复制引用地址,slice也是浅拷贝concatconcat()方法用于合并两个或多个数组。此方法不会改变现有数组,而是返回一个新的数组兼容对象:Arrayconstarray1=['a','b',['c0','c1','c2']];常量数组2=数组1。concat();array2[1]='B';array2[2][1]='C1';console.log(array1);//['a','b',['c0','C1','c2']]console.log(array2);//['a','B',['c0','C1','c2']]//复制一层,第二层开始引用Concat和slice都是针对数组的浅拷贝。如何实现浅拷贝简单来说,浅拷贝只是拷贝一个对象的属性。typeoftarget==='object'&&target!==null){vartarget=Array.isArry(source)?[]:{};for(letpropinsource){if(source.hasOwnProperty(prop)){target[prop]=source[prop];}}返回目标;}else{返回源代码;}}综上所述,JavaScript浅拷贝有4种,数组浅拷贝的slice和concat,对象的Object。assign(),以及适用于数组和对象的扩展运算符(...)。深拷贝原理。浅拷贝只是创建一个新的对象,并拷贝原对象的基本类型的值,而引用类型只拷贝一层属性,更深层的拷贝是无法拷贝的。深拷贝是不同的。它会在堆内存中开辟一块内存地址,将原来的对象完整的复制过来。深拷贝是将一个对象从内存中完整复制到目标对象中,并从堆内存中开辟一个内存地址。一个新的空间用来存放新的对象,新对象的修改不会改变元对象。两人实现真正的分离。简单归纳:深拷贝递归复制各级对象的属性JSON.stringifyvararr=[1,2,3,4,{value:5}];vararr1=JSON.parse(JSON.stringify(arr));arr[4].value=6;console.log(arr1);//[1,2,3,4,{value:5}]varobj={name:"johan",address:{city:"shanghai"}}varobj1=JSON.parse(JSON.stringify(obj));obj.address.city="北京";console.log(obj1);//{name:"johan",address:{city:"shanghai"}JSON.stringify虽然可以实现数组和对象的深拷贝,但是有几个坑无法实现函数、RegExp等特殊对象的克隆.它会丢弃对象的构造函数,所有的构造函数都会循环引用指向Object对象,会报错。让我们测试一下这些陷阱,//ConstructorfunctionPerson(name){this.name=name;}constElaine=newPerson('elaine');//functionfunctionsay(){console.log('hi');}constoldObj={a:say,b:newArray(1),c:newRegExp('ab+c','i'),d:Elaine,};constnewObj=JSON.parse(JSON.stringify(oldObj));//不能复制函数console.log(newObj.a,oldObj.a);//undefined[Function:say]//稀疏数组复制错误console.log(newObj.b[0],oldObj.b[0]);//nullundefined//无法复制常规对象console.log(newObj.c,oldObj.c);//{}/ab+c/i//构造函数指向错误console.log(newObj.d.constructor,oldObj.d.constructor);//[Function:Object][Function:person]我们可以看到在函数中,正则对象稀疏数组等对象克隆时会出现意外,构造函数指向constoldObj={};oldObj.a=oldObj;constnewObj=JSON.parse(JSON.stringify(oldObj));console时也会出现错误.log(newObj.a,oldObj.a);//TypeError:将循环结构转换为JSON对象循环引用会抛错JSON.stringify深拷贝可以解决现实中的大部分场景,但它的缺陷也让它成为面试的常客,下面来挑战一下手写深拷贝。深拷贝的关键是递归+深拷贝。我们先实现一个数组和对象的深拷贝功能。deepClone(source){//对于基本数据类型if(typeofsource!=='object'||source===null){returnsource}//判断是数组还是对象//或者让target=源实例数组?[]:{}lettarget=Array.isArray(source)?[]:{}//遍历并复制每个属性for(letpropinsource){//只复制自己的属性if(source.hasOwnProperty(prop)){//判断自己的属性是否为对象target[prop]=typeofsource[prop]==='对象'?deepClone(source[prop]):source[prop]}}returntarget}上面是一个简单的深拷贝,JSON.stringify的深拷贝效果还不错。它们还有一个缺点,如果在包含循环引用(对象之间相互引用,形成无限循环)的对象上执行此方法,则会抛出错误。将忽略以Symbol类型作为属性值的属性。缺少其他内置结构函数的兼容性,比如Function,RegExp,Date,Set,Map我们使用WeakMap来解决循环引用,如果你想要其他的数据类型,这里补充一下为什么要用WeakMap来解决循环引用,以及它和Map的区别如果要解决循环引用问题,可以开辟额外的存储空间,用来存储当前对象和复制对象的对应关系。复制的时候,先从空格里找,找到了就直接返回。如果没有,则正常复制,这种数据结构可以使用map,WeakMap。两者之间的区别在于WeakMap对象是键/值对的集合,其中键是弱引用。它的键必须是对象,而值可以是任意的。Map的key可以是任意的,包括function、object或者任何基本类型。WeakMap是一个弱引用,可以被垃圾回收。Map的键绑定到内存。Map是可以遍历的,WeakMap是不能遍历的。简单的说,因为WeakMap是弱引用,所以在没有其他引用的情况下可以正常进行垃圾回收。functiondeepClone(source,storage=newWeakMap()){//对于基本数据类型if(typeofsource!=='object'||source===null){returnsource}//是否是日期if(source.constructor===Date){returnnewDate(source)}//它是正则的if(source.constructor===RegExp){returnnewRegExp(source)}//它是一个数组lettarget=sourceinstanceof大批?[]:{}//循环引用返回存储的引用数据if(storage.has(source))returnstorage.get(source)//开辟存储空间并设置临时存储值storage.set(source,target)//是否包含Symbol类型letisSymbol=Object.getOwnPropertySymbols(source)//包含Symbol类型if(isSymbol.length){isSymbol.forEach((item)=>{if(typeofsource[item]==='object'){target[item]=deepClone(source[item],storage);return}target[item]=source[item]})}//不包含符号for(letkeyinsource){if(source.hasOwnProperty(key)){target[key]=typeofsource[key]==='object'?deepClone(sourcep[key],storage):source[key]}}returntarget;}作者的深拷贝肯定不是最全的。非大佬写出的让面试官惊叹的深文案,堪比。作者只能说提供了几个深拷贝。前端面试的必考项目之一。如果他问你怎么手写,如果你只写JSON.parse(JSON.stringify(source)),你肯定不合格。写hasOwnProperty只能靠边站,如果你解决了循环引用、Symbols、各种数据类型的拷贝等问题,就说明你懂得如何使用slice和concat深浅结合,写出一篇惊艳的深拷贝引用采访copiesofarrays前端闲聊在院子里的官方深拷贝——如何在一篇文章中摸索出ES拷贝的深度如何写深拷贝惊艳面试官?深入理解JavaScript系列文章——深入理解JavaScript开篇——什么是JavaScript深入理解JavaScript——JavaScript是由什么组成的深入理解JavaScript——万物皆对象深入理解JavaScript-Object(对象)深入理解JavaScript-whatnewdid深入理解JavaScript-Object.create深入理解JavaScript-copy的秘密深入理解JavaScript-in-深入理解JavaScript——Inheritance深入理解JavaScript——JavaScript中的始祖深入理解JavaScript——instanceof——寻祖深入理解JavaScript——Function深入理解JavaScript——Scope深入理解JavaScript——深入理解this关键字JavaScript——call,apply,bind将深入理解JavaScript——immediatelyexecuted函数(IIFE)深入理解JavaScript——词法环境深入理解JavaScript——执行上下文和调用栈深入理解JavaScript——作用域VS执行上下文深入理解JavaScript——ClosureIn-深入理解JavaScript——防抖与节流深入理解JavaScript——函数式编程深入理解JavaScript——垃圾回收机制深入理解JavaScript——数组深入理解JavaScript——Cyclescomehere深入JavaScript的理解————字符串