前言
新鲜事物肯定是最好的!
你多半在清楚认识 JavaScript 拷贝之前就已经使用过它了。或许你也听过这个规范:在函数式编程中,你不应该随意操作任何现存数据(译:感觉有点突兀,水平有限)。这篇文章将向你讲述如何在 JavaScript 中安全地拷贝一个值,并避免引发意外的错误。
什么是拷贝 ?
一个东西的拷贝看起来像是原来的东西,然而它并不是。同时,当你改变拷贝时,原来的东西可不会发生变化。
在编程时,我们把值存储在变量里,拷贝意味着用原变量初始化了一个新变量。请注意,拷贝具有两个不同的概念:深拷贝(deep copying) 与 浅拷贝(shallow copying)。深拷贝意味着新变量的所有值都被复制且与原变量毫不相关;浅拷贝则表示着新变量的某些(子)值仍然与原变量相关。
为了更好的理解深拷贝与浅拷贝,我们需要知道,JavaScript 是如何存储一个值的。
值的存储方式
原始数据类型
原始数据类型包括:
- Number 如:
1
- String 如:
'Hello'
- Boolean 如:
true
- undefined
- null
这些类型的值与指定给它们的变量紧密相连,也不会同时与多个变量关联,这意味着你并不需要担心在JavaScript 中复制这些原始数据类型时发生意外:复制它们得到的是一个确确实实独立的副本。
我们来看一个例子:
const a = 5
let b = 6 // 创建 a 的拷贝
console.log(b) // 6
console.log(a) // 5
通过执行 b = a
,就可以得到 a 的拷贝。此时,将新值重新指定给 b 时,b 的值会改变,但 a 的值不会随之发生变化。
复合数据类型—— Object 与数组
技术上看,数组也是 Object 对象,所以它们有着相似的表现。关于这点,后文我们会详细地介绍。
在这里,拷贝变得耐人寻味了起来:复合类型的值在被实例化时仅会被创建一次。也就是说,如果我们进行复合类型的拷贝,实际上是分配给拷贝一个指向原对象的引用。
const a = {
en: 'Hello',
de: 'Hallo',
es: 'Hola',
pt: 'Olà'
}
let b = a
b.pt = 'Oi'
console.log(b.pt) // Oi
console.log(a.pt) // Oi
上面的实例展示了浅拷贝的特征。通常而言,我们并不期望得到这种结果——原变量 a
并不应该受到新变量 b
的影响。当我们访问原变量时,往往造成出乎意料的错误。因为你不清楚错误的原因,可能会在造成错误后进行一会儿的调试,接着“自暴自弃”了起来。
不用急,让我们看看一些实现深拷贝的方法。
实现深拷贝的方法
Object
有许多方法可以确实地复制一个对象,其中新的 JavaScript 规范提供了我们一种非常快捷的方式。
展开运算符(Spread operator)
它在 ES2015 中被引入,它太吊了,因为它实在是简洁方便。它可以把原变量“展开”到一个新的变量中。使用方式如下:
const a = {
en: 'Bye',
de: 'Tschüss'
}
let b = {...a} // 没错!就这么简单
b.de = 'Ciao'
console.log(b.de) // Ciao
console.log(a.de) // Tschüss
也可以使用它把两个对象合并在一起,例如 const c = {... a,... b}
。
Object.assign
这种方法在展开运算符出现之前被广泛采用,基本上与后者相同。但在使用它时你可得小心,因为 Object.assign()
方法的第一个参数会被修改然后返回,所以一般我们会传给第一个参数一个空对象,防止被意外修改。然后,传你想复制的对象给第二个参数。
const a = {
en: 'Bye',
de: 'Tschüss'
}
let b = Object.assign({}, a)
b.de = 'Ciao'
console.log(b.de) // Ciao
console.log(a.de) // Tschüss
陷阱:嵌套的 Object 对象
在复制一个对象时有个很大的陷阱,你也许也发现了,这个陷阱存在于上述的两种拷贝方法:当你有一个嵌套的对象(数组)并试图深拷贝它们时。该对象内部的对象并不会以同样的方式被拷贝下来——它们会被浅拷贝。因此,如果你更改得到的拷贝里的对象,原对象里的对象也将改变。下面是此错误的示例:
const a = {
foods: {
dinner: 'Pasta'
}
}
let b = {...a}
b.foods.dinner = 'Soup' // dinner 并未被深拷贝
console.log(b.foods.dinner) // Soup
console.log(a.foods.dinner) // Soup
要得到让对象里的对象得到预期的深拷贝,你必须手动复制所有嵌套对象:
const a = {
foods: {
dinner: 'Pasta'
}
}
let b = {foods: {...a.foods}}
b.foods.dinner = 'Soup'
console.log(b.foods.dinner) // Soup
console.log(a.foods.dinner) // Pasta
如果要拷贝的对象里不止一个对象( foods
),可以再次利用一下展开运算符。也就是这样:const b = {... a,foods:{... a.foods}}
。
简单粗暴的深拷贝方式
如果你不知道对象有多少层嵌套呢?手动遍历对象并手动复制每个嵌套对象可十分繁琐。有一种方法能粗暴地拷贝下对象。只需将对象转换为字符串(stringify),然后解析一下(parse)它就完事啦:
const a = {
foods: {
dinner: 'Pasta'
}
}
let b = JSON.parse(JSON.stringify(a))
b.foods.dinner = 'Soup'
console.log(b.foods.dinner) // Soup
console.log(a.foods.dinner) // Pasta
如果使用这种方法,你得明白这是无法完全复制自定义类实例的。所以只有拷贝仅有 本地JavaScript值(native JavaScript values) 的对象时才可以使用此方式。
水平不够,翻译不好?,放下原文:
建议先不纠结,后文有细说。
数组
拷贝数组和拷贝对象相仿,因为数组本质上也是一种对象。
展开运算符
操作起来和对象一样:
const a = [1,2,3]
let b = [...a]
b[1] = 4
console.log(b[1]) // 4
console.log(a[1]) // 2
数组方法:map, filter, reduce
运用这些方法可以得到一个新的数组,里面包含原数组里的所有值(或部分)。在拷贝过程中还可以修改你想修改的值,上帝啊,这也太方便了吧。
const a = [1,2,3]
let b = a.map(el => el)
b[1] = 4
console.log(b[1]) // 4
console.log(a[1]) // 2
或者在复制时修改所需的元素:
const a = [1,2,3]
const b = a.map((el, index) => index === 1 ? 4 : el)
console.log(b[1]) // 4
console.log(a[1]) // 2
Array.slice
slice
方法通常用于返回数组的子集。数组的子集从数组的特定下标开始,也可以自定义结束的位置。使用 array.slice()
或 array.slice(0)
时,可以得到 array
数组的拷贝。
const a = [1,2,3]
let b = a.slice(0)
b[1] = 4
console.log(b[1]) // 4
console.log(a[1]) // 2
多维数组(Nested arrays,嵌套数组)
和 Object 一样,使用上面的方法并不会将内部元素进行同样的深拷贝。为了防止意外,可以使用JSON.parse(JSON.stringify(someArray))
。
奖励(BONUS):复制自定义类的实例
当你已是专业的 JavaScript 开发人员,并也要复制自定义构造函数或类时,前面已有提到:你不能简单地将他们转为字符串然后解析,否则实例的方法会遗失。Don't panic!可以自己定义一个 Copy
方法来得到一个具有所有原对象值的新对象,看看具体实现:
class Counter {
constructor() {
this.count = 5
}
copy() {
const copy = new Counter()
copy.count = this.count
return copy
}
}
const originalCounter = new Counter()
const copiedCounter = originalCounter.copy()
console.log(originalCounter.count) // 5
console.log(copiedCounter.count) // 5
copiedCounter.count = 7
console.log(originalCounter.count) // 5
console.log(copiedCounter.count) // 7
如果要将对象内部的对象也运用深拷贝,你得灵活使用有关深拷贝的新技能。我将为自定义构造函数的拷贝方法添加最终的解决方法,使它更加动态。
使用此拷贝方法,你可以在构造函数中防止任意数量地值,而不再需要一一赋值。
(译:我觉得这奖励是个作业)
结尾:
原文作者:Lukas Gisder-Dubé
原文:How to differentiate between deep and shallow copies in JavaScript
译文和原文有所出入(大意一致),翻译不易,点个赞bie~嘿嘿嘿
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!