深複製

物件的深複製是指一個副本,其屬性不與源物件(建立該副本的原始物件)共享相同的引用(指向相同的底層值)。因此,當你改變源物件或副本時,你可以確信不會導致另一個物件也發生改變。這種行為與淺複製的行為形成對比,在淺複製中,對源物件或副本中巢狀屬性的更改可能會導致另一個物件也發生改變。

如果兩個物件o1o2的可觀察行為相同,則它們是“結構等價”的。這些行為包括:

  1. o1o2的屬性具有相同的名稱和順序。
  2. 它們屬性的值是結構等價的。
  3. 它們的原型鏈是結構等價的(儘管當我們處理結構等價時,這些物件通常是普通物件,這意味著它們都繼承自Object.prototype)。

結構等價的物件可以是同一個物件(o1 === o2),也可以是“副本”(o1 !== o2)。由於等價的原始值總是比較相等,因此你無法建立它們的副本。

我們現在可以更正式地定義深複製為:

  1. 它們不是同一個物件(o1 !== o2)。
  2. o1o2的屬性具有相同的名稱和順序。
  3. 它們屬性的值是彼此的深複製。
  4. 它們的原型鏈是結構等價的。

深複製的原始鏈可能被複制,也可能不被複制(通常不復制)。但是,具有結構不等價原型鏈的兩個物件(例如,一個是陣列而另一個是普通物件)永遠不是彼此的副本。

一個物件(其所有屬性都具有原始值)的副本符合深複製和淺複製的定義。然而,談論這種複製的“深度”有些無用,因為它沒有巢狀屬性,而我們通常在修改巢狀屬性的上下文中使用深複製。

在 JavaScript 中,標準內建的物件複製操作(展開語法Array.prototype.concat()Array.prototype.slice()Array.from()Object.assign())不建立深複製(相反,它們建立淺複製)。

如果一個 JavaScript 物件可以序列化,那麼一種建立深複製的方法是使用JSON.stringify()將物件轉換為 JSON 字串,然後使用JSON.parse()將字串轉換回一個(全新的)JavaScript 物件。

js
const ingredientsList = ["noodles", { list: ["eggs", "flour", "water"] }];
const ingredientsListDeepCopy = JSON.parse(JSON.stringify(ingredientsList));

因為深複製不與其源物件共享任何引用,所以對深複製進行的任何更改都不會影響源物件。

js
// Change the value of the 'list' property in ingredientsListDeepCopy.
ingredientsListDeepCopy[1].list = ["rice flour", "water"];
// The 'list' property does not change in ingredients_list.
console.log(ingredientsList[1].list);
// Array(3) [ "eggs", "flour", "water" ]

然而,儘管上面程式碼中的物件足夠簡單,可以序列化,但許多 JavaScript 物件根本無法序列化——例如,函式(帶閉包)、Symbol、代表 HTML DOM API 中 HTML 元素的物件、遞迴資料以及許多其他情況。在這些情況下呼叫JSON.stringify()來序列化物件將失敗。因此,無法建立此類物件的深複製。

Web API structuredClone()也建立深複製,其優點是允許源中的可轉移物件被“轉移”到新副本中,而不僅僅是克隆。它還處理更多資料型別,例如Error。但請注意,structuredClone()本身不是 JavaScript 語言的特性——它是實現 Web API 的瀏覽器和其他 JavaScript 主機的特性。並且呼叫structuredClone()來克隆不可序列化物件將失敗,就像呼叫JSON.stringify()來序列化它會失敗一樣。