javascript
如何在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 = 5let b = a // this is the copyb = 6console.log(b) // 6console.log(a) // 5By 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的嵌套值,因為a和b實際上指向同一事物。 例:
const a = {en: 'Hello',de: 'Hallo',es: 'Hola',pt: 'Olà'}let b = ab.pt = 'Oi'console.log(b.pt) // Oiconsole.log(a.pt) // OiIn 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) // Ciaoconsole.log(a.de) // TschüssYou 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) // Ciaoconsole.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 objectsconsole.log(b.foods.dinner) // Soupconsole.log(a.foods.dinner) // SoupTo 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) // Soupconsole.log(a.foods.dinner) // PastaIn 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) // Soupconsole.log(a.foods.dinner) // PastaHere, 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] = 4console.log(b[1]) // 4console.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] = 4console.log(b[1]) // 4console.log(a[1]) // 2Alternatively 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]) // 4console.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] = 4console.log(b[1]) // 4console.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.countreturn copy}}const originalCounter = new Counter()const copiedCounter = originalCounter.copy()console.log(originalCounter.count) // 5console.log(copiedCounter.count) // 5copiedCounter.count = 7console.log(originalCounter.count) // 5console.log(copiedCounter.count) // 7To 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/
總結
以上是生活随笔為你收集整理的如何在JavaScript中区分深层副本和浅层副本的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 梦到偶像死了是什么征兆
- 下一篇: 为什么会梦到自己会飞