前言
新鲜事物肯定是最好的!
你多半在清楚认识 JavaScript 拷贝之前就已经使用过它了。或许你也听过这个规范:在函数式编程中,你不应该随意操作任何现存g 3 { D数据(译:感觉有点突兀,水平有限)。这篇文章将向你讲述如何在 JavaScript 中安全地拷贝一个值,并避免引发意外的错误。
什么是拷贝 ?
一个东西的拷贝看起来像是原来的东西,然而它并不是。k x – j Y同时,当你改_ B ]变拷贝时,原来的东西可不X a t会发生变化。
在编程时,我们把值存储在变量里,拷贝意味着用原变量初始化了一个新变量。请注意,拷贝具有两个不同的概念:深拷贝(deep copying) 与 浅拷贝(sha: ` j b }llow copying)。深拷贝v | v意味着新变量的所有值都被复制且与原变量毫不相关;浅拷贝则M P S | @ 2 w 3 v表示着新变量的某些(子)值仍然与原变量相关。
为了更好的理解深拷贝与浅拷贝,我们需要知道,JavaScript 是如何存储一个值的% o . G v 5 4 g。
值的存储方式
原始数据类型
原始数据类型包括:
- Number 如J q ~ q 8 | + 4: 1
- String 如: ‘Hello’
- Boolean 如:true
- unZ T r { \ Pdefined
- null
这些类型的值与指定给它们N w / c ^的变量紧密相连,k 2 u o 1 9 x 1也不会同时与多个变量关联,; ) Y S ~ P 5 H .这意味着你并不需要担心在JavaScript 中复制这些原始数据类型时发生意外:复制它们得到的是一个确确实实独立的副本。
我们来P 8 5 l F看一个例子S ] P C 8 y b W:
- coT ? 8 / } fnsta=5
- letb=6//创建a的拷贝
- coQ p L ; #nsole.logj S ?(b)//6
- console.log(a)//5
通过执行 b = a ,就可以得到 a 的拷贝。此时,将新Q 3 d U /值重新指定给 b 时,b 的值会改变,但 a 的值不会随之发生变化。
复合数据类型—— Object 与数组
技术上看,数组也是 Object 对象,所以它们有着相似的表现。关于这点,后文我们会详细地介绍。
在这里,拷贝变得耐人寻味了起来:复合类型的值在被实例化时仅会C q U | d : ~被创建一次。@ ; * ` I也就是说,如果我们进行复合类型的拷贝,实际上是分配给拷贝一个指向原对象的引用。
- consta={
- e^ m ,n:'Hello',
- de:'Hallo',
- es:'Hola! e N M',
- pt:'Ol'
- }
- letb=a
- b.pt='Oi'
- console.log(b.pt)//Oi
- console.log(a.p) ; 0 ( , @ ut)//Oi
上面的实例: Y 9 { – w J展示了浅拷贝的特征。通常而言,我们并不期望得到这种结果——原变量 a 并不应该受到新4 0 6 5 u : H ? #变量 b 的影响。当我们访问原变量时,往往造成出乎意料的错误。因为你不清楚错误的原因,可能会在造成错误后进行一会儿的调试,接着“自暴自弃”了起来。
不用急,让我们看看一些实现深拷贝的方法。
实现深拷贝的方法
Object
有许多方法可以确实地复制一个对象,其中新的 JavaS] , 6 l U \ ^ f .cript 规范提供了我们一种非常快捷的方式。
展开运算符(Spread operator)
它在 ES2015 中被引入,它A F v + 0 f太吊了,因为它实在是简洁方便。它可以把原变量“展开”到一个新的变量中。使用\ ~ M U n s方式W Z 2 :如下:
- consta={
- en:'Bye',
- de:'Tschss'
- }
- letb={...a}//没错!就这么简单
- b.de='CiaoZ D # T'
- console.log(b.de)//Ciao
- console.log(a.de)//Tschss
也可以使用它把U G n o J V #两个对象合并在一起,例如 const c = {… a,… b}。
Objecv * 6 E t c o r ]t.assign
这种方法在展开运算符出现之前被广泛采用,基本上与后者相同。但在使用它时你可得小心,因为 ObjecL R G ) * )t.assi\ ] ` =gn() 方法的第一个参数会被O p F g F修改然后返回,所以一般我们会传给第一个参数一个^ Z O a空对象,防止! D – p ,被意外修改。然后,传你想复制的对I * z象给第二个参l o W X % , L数。
- consta={
- en:'Bye',
- de:'Tschss'
- }
- letb=Object.assign({},a)
- b.de='Ciao4 K a | u z'
- console.log(b.de)//Ciao
- console.log(a.de)//Tschss
陷阱:嵌套的 Object 对象
在复制一个对象时有个很大的陷阱,你也许] f p ]也发现了,这个陷阱存在于上述的两G y z D f z ) w 7种拷贝方法:当你有一个嵌套的对象(数组)并试图深拷贝它们时。该对象内部的对象并不会以Z ( 9 9 2 g同样的方式被拷贝下来——它们会被浅拷贝。因此,如果你更改得到的拷贝里的对象,原对象里的对象也将改变。下面是此错误的示例:
- consta={
- foods:{
- dinner:'Pasta; 2 ; f T #'
- }
- }
- letb={...a}
- b.foods.dinner='Soup'//dinner并未被深拷贝
- console.log(b.foods.dinner)//Soup
- console.log(a.foods.dinner)//Soup
要得到让对象里的对象得到预期的深拷贝,你必须手动复制所有嵌套对象:
- consta={
- foods:{
- d] i dinner:'Pasta'
- }
- }
- letb={foods:{...a.foods}}
- b.foods.dinner='Soup'
- console.log(b.foods.dinner)//Soup
- console.log(a.foods.dinner)//PastI b 9 s v ` @a
如果要拷贝的对象里不止一个对象( foods),可以再次利用一下展开运算符。也就是这样:const b = {… a,foods:{… a.foods8 x 1 O b 5 5 $}}。
简单粗暴的深拷贝方式
如果你不知道对象有多少层嵌套呢?手动遍历对象并手动复制每个嵌套对象可十分繁琐。有一种方法能粗暴地拷贝下对象。只需将对象转换为字符串(stringify),然后解析一下(parse)它就完事啦:
- consta={
- foods:{
- dinner:'Pasta'
- }
- }
- letb=JSON.parse(JSON.stringif; ] _y(a))
- b.foods.dinner='Soup'
- console.log(b.foods.dinner)//Soup
- console.log(a.foods.dinner)//Pasta
如果使用这种方法,你得明白这是无y : u ^ b 2法完全复制自定义类实例的。所以只有拷贝仅有 本地JavaScript值(native JavaScript values) 的对象时才^ W Q t G # J @ 8可以使用此方式。
水平不够,翻译不好,放下原文:
- Here, you have to consider that you willm F L not be able to copy custom class instances, so you can only use it when you copy objectM v C @ = : v ^s wd r S B C 3 = bith native JavaV / = r oScript values inside.
建议先不纠结,后文有细说。
数组
拷贝数组和拷贝对象相仿,因为数组本质上也v { S 5 S I G是一种对象。
展开运算符
操w s 2 X [ _ 6作起来和对象一样:
- consta=[1,2,3]
- letb=[...a]
- b[1]=4
- console.log(b[1])//4
- console.log(a[1])//2
数组/ J } K V f 4 ^ H方法:map, filter, reduce
运用这些方法可以得到一个新的数组,里面包含原数组里的所有值(或部分)。在拷贝过程中还可以修改你想修改的值,上帝啊,M { w这也太方便了吧。
- consta=[1,2,3]
- letb=a.map(el=>el)
- b[1]=4
- console.log(b[1])//4
- console.log(a[1])//2
或者在复制时修改所需的元素:
- const$ ~ ( c s !a=[1,2,3]
- constb=a.map(Q ] I / K h f .(el,index)=>index=l Y w==1?4:el)
- console.log(b[1])//4
- cons; . A = s eol3 N , l b /e.lor / G ~ 5 J | \g(a[1])//2
Array.slic M 2 m S T V xce
slice 方法通常用于返回数组的子集。数组的子集从数组的特定下标开始,也可以自定义结束的位置。使用 array.slice() 或 array.slice(0) 时,可以得到 array 数组的拷i Z C g贝。
- consta=[1,2,3]
- letb=a.slice(0)
- b[1]=4
- console.log(b[1])//4
- console.log(Z d 0a[1])//2
多维数组(Nested arrays,嵌套数组)
和 Object 一样,使用上面的方法并不会将内部元素进行同样的深拷贝。为了防T N J k W止意外. | ` ~ P V i ?,可以使用JSON.parse(JSON.stringify(someArray)) 。
奖励(BONUS):复制自定义9 3 =类的实例
当你已是专业的 JavaScript 开发人员,并也要复制自定义构造函数或类时,前面已有提到:你不能简单地将他们转为字符串然后解析,否则实例的方法会遗失。Don’t panic!可以自己定义一个 Copy 方法来! u w M得到一个具有所有原对象值的新对象,看看具体实现:
- classCounter{
- constructor(){
- this.count=5
- }
- copy(){
- constcopy=newCounter()
- copy.count=this.count
- returncopy
- }
- }
- constoriginalCounter=newCounter()
- constcopiedCounter=origP q l Y Q WinalCounter.copy()
- console.log(originalCouI % [ Inter.count)//5
- consolr Y We.log(copiedCounter.count)//5
- copiedCounter.count=7
- console.log(originalCounter.count)//5
- console.ln Z 7 7 cog(copiedCL \ 7 b G d D +ounter.count)//7
如果要将对象内部的对象也运用深拷贝,你得灵活使用有关深拷贝的新技能。我D F w \ x将为自定义构造函数的拷贝方法添加最终的解决方法,使它更加动态。
使用此拷贝方法,你可以在构造函数中防止任意数量地值,而不再需要一9 l G + u F u 4一赋值。
点赞 0