前言新的永远是最好的!您很可能在清楚地了解它之前就已经使用过JavaScriptcopy。或许你也听过这样的规范:在函数式编程中,你不应该随意操作任何已有的数据(译:感觉有点别扭,水平有限)。本文将向您展示如何在JavaScript中安全地复制值而不会引发意外错误。什么是副本?某物的副本看起来像原物,但事实并非如此。此外,当您更改副本时,原件不会更改。编程时,我们把值存入变量,复制就是用原来的变量初始化一个新的变量。请注意,复制有两个不同的概念:深复制和浅复制。深拷贝是指复制新变量的所有值,与原变量无关;浅拷贝意味着新变量的某些(子)值仍然与原始变量相关。为了更好地理解深拷贝和浅拷贝,我们需要知道JavaScript是如何存储一个值的。值的存储方式原始数据类型原始数据类型包括:数字如:1字符串如:'Hello'布尔型如:trueundefinednull这些类型的值与赋给它们的变量密切相关,并将不会同时与多个变量相关联,这意味着您在JavaScript中复制这些原始数据类型时无需担心出现意外:您复制的是一个完全独立的副本。我们来看一个例子:consta=5letb=6//Createacopyofaconsole.log(b)//6console.log(a)//5通过执行b=a,可以得到a的副本。在这种情况下,当给b重新赋值时,b的值发生了变化,而a的值并没有相应的变化。复合数据类型-对象和数组从技术上讲,数组也是Object对象,因此它们的行为相似。关于这一点,我们稍后会详细介绍。这就是复制变得有趣的地方:复合类型的值仅在实例化时创建一次。也就是说,如果我们对复合类型进行复制,实际上是将对原始对象的引用分配给该副本。consta={en:'Hello',de:'Hallo',es:'Hola',pt:'Olà'}letb=ab.pt='Oi'console.log(b.pt)//Oiconsole.log(a.pt)//Oi上面的例子说明了浅拷贝的特点。通常,我们不希望出现这种结果——原始变量a不应受到新变量b的影响。当我们访问原始变量时,往往会导致意想不到的错误。因为不知道错误的原因,导致错误后可能会调试一段时间,然后“自暴自弃”。别着急,让我们看看一些实现深拷贝的方法。实现深拷贝对象的方法有很多方法可以真正拷贝一个对象,而新的JavaScript规范为我们提供了一种非常快捷的方式。展开运算符(Spreadoperator)是ES2015引入的,太悬了,因为它太简洁方便了。它可以将原始变量“扩展”为新变量。用法如下:consta={en:'Bye',de:'Tschüss'}letb={...a}//对!就是这么简单b.de='Ciao'console.log(b.de)//Ciaoconsole.log(a.de)//Tschüss也可以用它来合并两个对象,例如constc={..。a,...b}。Object.assign这种方法在展开运算符之前被广泛采用,与后者基本相同。但是使用的时候一定要小心,因为Object.assign()方法的第一个参数会被修改然后返回,所以一般我们都会给第一个参数传递一个空对象,防止误修改。然后,将要复制的对象传递给第二个参数。consta={en:'Bye',de:'Tschüss'}letb=Object.assign({},a)b.de='Ciao'console.log(b.de)//Ciaoconsole.log(a.de)//Tschüsstrap:nestedObjectobjects在复制对象时有一个很大的陷阱,你可能也发现了这个陷阱存在于上面的两种复制方法中:当你有一个嵌套对象(数组)并试图对其进行深复制时。该对象内的对象不会以相同的方式复制下来——它们是浅复制的。因此,如果更改结果副本中的对象,则原始对象中的对象也会更改。这是此错误的示例:consta={foods:{dinner:'Pasta'}}letb={...a}b.foods.dinner='Soup'//dinner没有深度复制console.log(b.foods.dinner)//Soupconsole.log(a.foods.dinner)//Soup要获得Soup内部对象的预期深拷贝,必须手动复制所有嵌套对象:Pasta'}}letb={foods:{...a.foods}}b.foods.dinner='Soup'console.log(b.foods.dinner)//Soupconsole.log(a.foods.dinner)//Pasta如果要复制的对象中有多个对象(foods),可以再次使用扩展运算符。就是这样:constb={...a,foods:{...a.foods}}。简单粗暴的深拷贝方法如果你不知道一个对象嵌套了多少层怎么办?手动遍历对象并手动复制每个嵌套对象可能非常繁琐。有一种方法可以粗略地复制对象。只需将对象转换为字符串(stringify),然后解析(parse)即可:consta={foods:{dinner:'Pasta'}}letb=JSON.parse(JSON.stringify(a))b.foods.dinner='Soup'console.log(b.foods.dinner)//Soupconsole.log(a.foods.dinner)//Pasta如果用这个方法,你要明白完全复制是不可能的自定义类实例。所以这个方法只能在复制只有原生JavaScript值(nativeJavaScriptvalues)的对象时使用。水平不够,翻译的不好,放下原文:这里,你要考虑到你将无法复制自定义类实例,所以只能在复制原生JavaScript值的对象时使用里面。建议先不要纠结,再正文详谈。Array复制数组类似于复制对象,因为数组本质上是一个对象。传播运算符就像对象一样工作:consta=[1,2,3]letb=[...a]b[1]=4console.log(b[1])//4console.log(a[1])//2个数组方法:map,filter,reduce使用这些方法得到一个新的数组,其中包含原数组中的所有值(或部分)。还可以在复制过程中修改自己想修改的值,天哪,这也太方便了吧。consta=[1,2,3]letb=a.map(el=>el)b[1]=4console.log(b[1])//4console.log(a[1])//2或者在复制时修改需要的元素:const=[1,2,3]constb=a.map((el,index)=>index===1?4:el)console.log(b[1])//4console.log(a[1])//2Array.slices方法通常用于返回数组的子集。数组的子集从数组中的特定下标开始,也可以自定义结束位置。使用array.slice()或array.slice(0)时,可以获得数组数组的副本。consta=[1,2,3]letb=a.slice(0)b[1]=4console.log(b[1])//4console.log(a[1])//2个多维数组(嵌套数组,nestedarray)和Object一样,使用上面的方法并没有对内部元素做同样的深拷贝。为了防止意外,可以使用JSON.parse(JSON.stringify(someArray))。奖励:复制自定义类的实例当你是一名专业的JavaScript开发人员并且想要复制自定义构造函数或类时,已经提到过:你不能简单地将它们转换为字符串然后进行解析,否则实例的方法将会丢失。不要恐慌!可以自己定义一个Copy方法,得到一个新的对象,所有的对象都是原来的值,具体实现见:classCounter{constructor(){this.count=5}copy(){constcopy=newCounter()copy.count=this.countreturncopy}}constoriginalCounter=newCounter()constcopiedCounter=originalCounter.copy()console.log(originalCounter.count)//5console.log(copiedCounter.count)//5copiedCounter.count=7console.log(originalCounter.count)//5console.log(copiedCounter.count)//7如果要对对象内部的对象进行深拷贝,就要灵活运用深拷贝的新技巧。我将为自定义构造函数的复制方法添加最终解决方法,以使其更具动态性。使用这种复制方法,你可以在构造函数中存储任意数量的值,而不需要一个一个地赋值。
