前言

新鲜事物肯定是最好的!

你多半在清楚认识 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

  1. coT ? 8 / } fnsta=5
  2. letb=6//创建a的拷贝
  3. coQ p L ; #nsole.logj S ?(b)//6
  4. console.log(a)//5

通过执行 b = a ,就可以得到 a 的拷贝。此时,将新Q 3 d U /值重新指定给 b 时,b 的值会改变,但 a 的值不会随之发生变化。

复合数据类型—— Object 与数组

技术上看,数组也是 Object 对象,所以它们有着相似的表现。关于这点,后文我们会详细地介绍。

在这里,拷贝变得耐人寻味了起来:复合类型的值在被实例化时仅会C q U | d : ~被创建一次。@ ; * ` I也就是说,如果我们进行复合类型的拷贝,实际上是分配给拷贝一个指向原对象的引用。

  1. consta={
  2. e^ m ,n:'Hello',
  3. de:'Hallo',
  4. es:'Hola! e N M',
  5. pt:'Ol'
  6. }
  7. letb=a
  8. b.pt='Oi'
  9. console.log(b.pt)//Oi
  10. 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 :如下:

  1. consta={
  2. en:'Bye',
  3. de:'Tschss'
  4. }
  5. letb={...a}//没错!就这么简单
  6. b.de='CiaoZ D # T'
  7. console.log(b.de)//Ciao
  8. 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数。

  1. consta={
  2. en:'Bye',
  3. de:'Tschss'
  4. }
  5. letb=Object.assign({},a)
  6. b.de='Ciao4 K a | u z'
  7. console.log(b.de)//Ciao
  8. console.log(a.de)//Tschss

陷阱:嵌套的 Object 对象

在复制一个对象时有个很大的陷阱,你也许] f p ]也发现了,这个陷阱存在于上述的两G y z D f z ) w 7种拷贝方法:当你有一个嵌套的对象(数组)并试图深拷贝它们时。该对象内部的对象并不会以Z ( 9 9 2 g同样的方式被拷贝下来——它们会被浅拷贝。因此,如果你更改得到的拷贝里的对象,原对象里的对象也将改变。下面是此错误的示例:

  1. consta={
  2. foods:{
  3. dinner:'Pasta; 2 ; f T #'
  4. }
  5. }
  6. letb={...a}
  7. b.foods.dinner='Soup'//dinner并未被深拷贝
  8. console.log(b.foods.dinner)//Soup
  9. console.log(a.foods.dinner)//Soup

要得到让对象里的对象得到预期的深拷贝,你必须手动复制所有嵌套对象:

  1. consta={
  2. foods:{
  3. d] i dinner:'Pasta'
  4. }
  5. }
  6. letb={foods:{...a.foods}}
  7. b.foods.dinner='Soup'
  8. console.log(b.foods.dinner)//Soup
  9. 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)它就完事啦:

  1. consta={
  2. foods:{
  3. dinner:'Pasta'
  4. }
  5. }
  6. letb=JSON.parse(JSON.stringif; ] _y(a))
  7. b.foods.dinner='Soup'
  8. console.log(b.foods.dinner)//Soup
  9. 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作起来和对象一样:

  1. consta=[1,2,3]
  2. letb=[...a]
  3. b[1]=4
  4. console.log(b[1])//4
  5. console.log(a[1])//2

数组/ J } K V f 4 ^ H方法:map, filter, reduce

运用这些方法可以得到一个新的数组,里面包含原数组里的所有值(或部分)。在拷贝过程中还可以修改你想修改的值,上帝啊,M { w这也太方便了吧。

  1. consta=[1,2,3]
  2. letb=a.map(el=>el)
  3. b[1]=4
  4. console.log(b[1])//4
  5. console.log(a[1])//2

或者在复制时修改所需的元素:

  1. const$ ~ ( c s !a=[1,2,3]
  2. constb=a.map(Q ] I / K h f .(el,index)=>index=l Y w==1?4:el)
  3. console.log(b[1])//4
  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贝。

  1. consta=[1,2,3]
  2. letb=a.slice(0)
  3. b[1]=4
  4. console.log(b[1])//4
  5. 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得到一个具有所有原对象值的新对象,看看具体实现:

  1. classCounter{
  2. constructor(){
  3. this.count=5
  4. }
  5. copy(){
  6. constcopy=newCounter()
  7. copy.count=this.count
  8. returncopy
  9. }
  10. }
  11. constoriginalCounter=newCounter()
  12. constcopiedCounter=origP q l Y Q WinalCounter.copy()
  13. console.log(originalCouI % [ Inter.count)//5
  14. consolr Y We.log(copiedCounter.count)//5
  15. copiedCounter.count=7
  16. console.log(originalCounter.count)//5
  17. 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一赋值。

【责任编辑:未丽燕 TEL:(010)68476606】

点赞 0