如何在JavaScript中区分深层副本和浅层副本

by Lukas Gisder-Dubé

卢卡斯·吉斯杜比(LukasGisder-Dubé)

如何在JavaScript中区分深层副本和浅层副本 (How to differentiate between deep and shallow copies in JavaScript)

New is always better!

新总是更好!

You have most certainly dealt with copies in JavaScript before, even if you didn’t know it. Maybe you have also heard of the paradigm in functional programming that you shouldn’t modify any existing data. In order to do that, you have to know how to safely copy values in JavaScript. Today, we’ll look at how to do this while avoiding the pitfalls!

即使您不知道,您肯定也曾经用JavaScript处理过副本。 也许您还听说过函数式编程的范例,您不应修改任何现有数据。 为此,您必须知道如何安全地在JavaScript中复制值。 今天,我们将研究如何避免陷阱!

First of all, what is a copy?

首先,什么是副本?

A copy just looks like the old thing, but isn’t. When you change the copy, you expect the original thing to stay the same, whereas the copy changes.

副本看起来像旧的东西,但不是。 更改副本时,您希望原始内容保持不变,而副本会更改。

In programming, we store values in variables. Making a copy means that you initiate a new variable with the same value(s). However, there is a big potential pitfall to consider: deep copying vs. shallow copying. A deep copy means that all of the values of the new variable are copied and disconnected from the original variable. A shallow copy means that certain (sub-)values are still connected to the original variable.

在编程中,我们将值存储在变量中。 进行复制意味着您将启动一个具有相同值的新变量。 但是,有一个潜在的陷阱需要考虑: 深层复制浅层复制 。 深拷贝意味着新变量的所有值都将被复制并与原始变量断开连接 。 浅表副本意味着某些(子)值仍连接到原始变量。

To really understand copying, you have to get into how JavaScript stores values.
要真正理解复制,您必须了解JavaScript如何存储值。
原始数据类型 (Primitive data types)

Primitive data types include the following:

基本数据类型包括以下内容:

  • Number — e.g. 1

    数字-例如1

  • String — e.g. 'Hello'

    字符串—例如'Hello'

  • Boolean — e.g. true

    布尔值—例如, true

  • undefined

    undefined

  • null

    null

When you create these values, they are tightly coupled with the variable they are assigned to. They only exist once. That means you do not really have to worry about copying primitive data types in JavaScript. When you make a copy, it will be a real copy. Let’s see an example:

创建这些值时,它们会与分配给它们的变量紧密耦合。 它们仅存在一次。 这意味着您实际上不必担心在JavaScript中复制原始数据类型。 制作副本时,它将是真实副本。 让我们来看一个例子:

const a = 5
let b = a // this is the copy
b = 6
console.log(b) // 6
console.log(a) // 5

By executing b = a , you make the copy. Now, when you reassign a new value to b, the value of b changes, but not of a.

通过执行b = a ,您可以制作副本。 现在,当你重新分配一个新值b ,值b的变化,但不是a

复合数据类型-对象和数组 (Composite data types — Objects and Arrays)

Technically, arrays are also objects, so they behave in the same way. I will go through both of them in detail later.

从技术上讲,数组也是对象,因此它们的行为方式相同。 稍后,我将详细介绍它们。

Here it gets more interesting. These values are actually stored just once when instantiated, and assigning a variable just creates a pointer (reference) to that value.

在这里,它变得更加有趣。 这些值实际上在实例化时只存储一次,分配一个变量只会创建指向该值的指针(引用)

Now, if we make a copy b = a , and change some nested value in b, it actually changes a’s nested value as well, since a and b actually point to the same thing. Example:

现在,如果我们制作一个副本b = a ,并更改b某个嵌套值,则实际上也会更改a的嵌套值,因为ab实际上指向同一事物。 例:

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

In the example above, we actually made a shallow copy. This is often times problematic, since we expect the old variable to have the original values, not the changed ones. When we access it, we sometimes get an error. It might happen that you try to debug it for a while before you find the error, since a lot of developers do not really grasp the concept and do not expect that to be the error.

在上面的示例中,我们实际上制作了一个浅表副本 。 这通常是有问题的,因为我们期望旧变量具有原始值,而不是更改后的值。 当我们访问它时,有时会出现错误。 您可能会尝试在发现错误之前先对其进行调试,因为许多开发人员并未真正掌握该概念,也不希望这是错误。

Let’s have a look at how we can make copies of objects and arrays.

让我们看一下如何制作对象和数组的副本。

对象 (Objects)

There are multiple ways to make copies of objects, especially with the new expanding and improving JavaScript specification.

有多种方法可以复制对象,特别是在新的扩展和改进JavaScript规范中。

点差运算符 (Spread operator)

Introduced with ES2015, this operator is just great, because it is so short and simple. It ‘spreads’ out all of the values into a new object. You can use it as follows:

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

You can also use it to merge two objects together, for example const c = {...a, ...b} .

您还可以使用它来合并两个对象,例如const c = {...a, ...b}

对象分配 (Object.assign)

This was mostly used before the spread operator was around, and it basically does the same thing. You have to be careful though, as the first argument in the Object.assign() method actually gets modified and returned. So make sure that you pass the object to copy at least as the second argument. Normally, you would just pass an empty object as the first argument to prevent modifying any existing data.

这主要是在散布运算符出现之前使用的,基本上可以完成相同的操作。 但是,您必须要小心,因为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
陷阱:嵌套对象 (Pitfall: Nested Objects)

As mentioned before, there is one big caveat when dealing with copying objects, which applies to both methods listed above. When you have a nested object (or array) and you copy it, nested objects inside that object will not be copied, since they are only pointers / references. Therefore, if you change the nested object, you will change it for both instances, meaning you would end up doing a shallow copy again. Example:// BAD EXAMPLE

如前所述,在处理复制对象时有一个很大的警告,它适用于上面列出的两种方法。 当您有一个嵌套对象(或数组)并复制它时,该对象内部的嵌套对象将不会被复制,因为它们只是指针/引用。 因此,如果您更改嵌套对象,则将在两个实例中都对其进行更改,这意味着您最终将再次进行浅表复制 。 例子//坏例子

const a = {
foods: {
dinner: 'Pasta'
}
}
let b = {...a}
b.foods.dinner = 'Soup' // changes for both objects
console.log(b.foods.dinner) // Soup
console.log(a.foods.dinner) // Soup

To make a deep copy of nested objects, you would have to consider that. One way to prevent that is manually copying all nested objects:

要制作嵌套对象深层副本 ,您必须考虑到这一点。 防止这种情况的一种方法是手动复制所有嵌套对象:

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

In case you were wondering what to do when the object has more keys than only foods , you can use the full potential of the spread operator. When passing more properties after the ...spread , they overwrite the original values, for example const b = {...a, foods: {...a.foods}} .

如果您想知道当对象具有比foods更多的键时该怎么做,则可以利用散布算子的全部潜力。 在...spread之后传递更多属性时,它们将覆盖原始值,例如const b = {...a, foods: {...a.foods}}

深思熟虑地进行深拷贝 (Making deep copies without thinking)

What if you don’t know how deep the nested structures are? It can be very tedious to manually go through big objects and copy every nested object by hand. There is a way to copy everything without thinking. You simply stringify your object and parse it right after:

如果您不知道嵌套结构的深度怎么办? 手动浏览大对象并用手复制每个嵌套对象可能非常繁琐。 有一种无需思考即可复制所有内容的方法。 您只需将对象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

Here, you have to consider that you will not be able to copy custom class instances, so you can only use it when you copy objects with native JavaScript values inside.

在这里,您必须考虑到您将无法复制自定义类实例,因此仅当您复制内部具有JavaScript值的对象时才能使用它。

数组 (Arrays)

Copying arrays is just as common as copying objects. A lot of the logic behind it is similar, since arrays are also just objects under the hood.

复制数组与复制对象一样普遍。 它背后的很多逻辑都是相似的,因为数组也只是底层的对象。

点差运算符 (Spread operator)

As with objects, you can use the spread operator to copy an array:

与对象一样,您可以使用spread运算符复制数组:

const a = [1,2,3]
let b = [...a]
b[1] = 4
console.log(b[1]) // 4
console.log(a[1]) // 2
数组功能-映射,过滤,缩小 (Array functions — map, filter, reduce)

These methods will return a new array with all (or some) values of the original one. While doing that, you can also modify the values, which comes in very handy:

这些方法将返回一个新数组,其中包含原始数组的所有(或某些)值。 在此过程中,您还可以修改值,这非常方便:

const a = [1,2,3]
let b = a.map(el => el)
b[1] = 4
console.log(b[1]) // 4
console.log(a[1]) // 2

Alternatively you can change the desired element while copying:

或者,您可以在复制时更改所需的元素:

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)

This method is normally used to return a subset of the elements, starting at a specific index and optionally ending at a specific index of the original array. When using array.slice() or array.slice(0) you will end up with a copy of the original array.

此方法通常用于返回元素的子集,该元素的子集从原始数组的特定索引开始,并且可选地终止于原始数组的特定索引。 当使用array.slice()array.slice(0)您将得到原始数组的副本。

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)

Similar to objects, using the methods above to copy an array with another array or object inside will generate a shallow copy. To prevent that, also use JSON.parse(JSON.stringify(someArray)) .

与对象相似,使用上述方法将一个数组复制到另一个数组或对象内部将生成一个浅表副本 。 为了防止这种情况,还请使用JSON.parse(JSON.stringify(someArray))

奖励:复制自定义类的实例 (BONUS: copying instance of custom classes)

When you are already a pro in JavaScript and you deal with your custom constructor functions or classes, maybe you want to copy instances of those as well.

当您已经是JavaScript专业人士并且要处理自定义构造函数或类时,也许您也想复制这些实例。

As mentioned before, you cannot just stringify + parse those, as you will lose your class methods. Instead, you would want to add a custom copy method to create a new instance with all of the old values. Let’s see how that works:

如前所述,您不能仅仅对它们进行字符串化+解析,否则您将丢失类方法。 相反,您可能想添加一个自定义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

To deal with objects and arrays that are referenced inside of your instance, you would have to apply your newly learned skills about deep copying! I will just add a final solution for the custom constructor copy method to make it more dynamic:

要处理实例内部引用的对象和数组,您必须应用关于深度复制的新知识! 我将为自定义构造函数的copy方法添加最终解决方案,以使其更加动态:

With that copy method, you can put as many values as you want in your constructor, without having to manually copy everything!

使用该复制方法,您可以在构造函数中放置任意数量的值,而无需手动复制所有内容!

About the Author: Lukas Gisder-Dubé co-founded and led a startup as CTO for 1 1/2 years, building the tech team and architecture. After leaving the startup, he taught coding as Lead Instructor at Ironhack and is now building a Startup Agency & Consultancy in Berlin. Check out dube.io to learn more.

关于作者:LukasGisder-Dubé与他人共同创立并领导了一家初创公司担任CTO长达1 1/2年,建立了技术团队和架构。 离开创业公司后,他在Ironhack担任首席讲师的编码课程,现在正在柏林建立创业公司和咨询公司。 查看dube.io了解更多信息。

翻译自: https://www.freecodecamp.org/news/copying-stuff-in-javascript-how-to-differentiate-between-deep-and-shallow-copies-b6d8c1ef09cd/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值