使用 StructuredClone 在 JavaScript 中進行深度複製

該平台現在隨附 nativeClone(),用於深層連結的內建函式。

最長的時候,您必須設法讓解決方法和程式庫建立 JavaScript 值的深度複本。Platform 現在隨附 structuredClone(),這是用於深度複製的內建函式。

瀏覽器支援

  • 98
  • 98
  • 94
  • 15.4

資料來源

淺層複本

在 JavaScript 中複製值幾乎一律是「淺層」,而非「深度」。也就是說,如果變更的深層巢狀值,同樣也會在副本中看到。

如要在 JavaScript 中使用物件傳播運算子 ... 建立淺層複製,方法之一是:

const myOriginal = {
  someProp: "with a string value",
  anotherProp: {
    withAnotherProp: 1,
    andAnotherProp: true
  }
};

const myShallowCopy = {...myOriginal};

直接在淺層副本中新增或變更屬性只會影響副本,不會影響原始副本:

myShallowCopy.aNewProp = "a new value";
console.log(myOriginal.aNewProp)
// ^ logs `undefined`

不過,新增或變更深層巢狀屬性會影響副本和原始屬性:

myShallowCopy.anotherProp.aNewProp = "a new value";
console.log(myOriginal.anotherProp.aNewProp) 
// ^ logs `a new value`

運算式 {...myOriginal} 會使用 Spread 運算子myOriginal 的 (列舉) 屬性進行疊代作業。這個函式使用屬性名稱和值,並逐一指派給剛建立且空的空物件。因此,產生的物件在形狀中是相同的,但擁有專屬的屬性和值清單副本。系統也會複製這些值,不過 JavaScript 值處理這類原始值的方式與非原始值不同。MDN 的引用:

在 JavaScript 中,原始值 (原始值、原始資料類型) 是不是物件且沒有方法的資料。原始資料類型有七種:字串、數字、大整數、布林值、未定義、符號和空值。

MDN — Primitive

系統會將非原始值視為「參照」references處理,也就是說,複製值的動作其實只是複製同一個基礎物件的參照,進而產生淺層複製行為。

深層複製

淺層副本的對面就是深層副本。深度複製演算法也會逐一複製物件的屬性,但在找到另一個物件的參照時,會以遞迴方式叫用本身,建立該物件的副本。這非常重要,可以確保兩段程式碼不會意外共用物件,並在無意間操控彼此的狀態。

過去在 JavaScript 中建立值的深度副本,實在沒有簡單或好方法。許多人仰賴 Lodash 的 cloneDeep() 等第三方程式庫。這個問題最常見的解決方法是 JSON 駭客:

const myDeepCopy = JSON.parse(JSON.stringify(myOriginal));

事實上,這也是一項熱門的解決方法,即 V8 積極最佳化 JSON.parse(),特別是上述模式,以盡可能加快速度。速度更快,但也有一些缺點和三線:

  • 遞迴資料結構JSON.stringify()如果提供遞迴資料結構,就會擲回。使用已連結的清單或樹狀結構時,很容易會發生這種狀況。
  • 內建類型:如果值包含其他 JS 內建函式,例如 MapSetDateRegExpArrayBuffer,會擲回 JSON.stringify()
  • 函式JSON.stringify() 會安靜捨棄函式。

結構化複製

平台已經需要能在幾個地方建立 JavaScript 值的深層副本:在索引資料庫儲存 JS 值時,必須用到某種序列化形式,才能將 JS 值儲存在磁碟上,並於稍後反序列化以還原 JS 值。同樣地,透過 postMessage() 傳送訊息至 WebWorker 時,也需要將 JS 值從某個 JS 領域轉移至另一個 JS 領域。用於這個用途的演算法稱為「結構化本機副本」,但不久前,開發人員並不容易存取這個演算法。

但現在有變化了!經過修訂的 HTML 規格將公開名為 structuredClone() 的函式,該函式可完全執行該演算法,方便開發人員輕鬆建立 JavaScript 值的深度副本。

const myDeepCopy = structuredClone(myOriginal);

這樣就大功告成了!這是整個 API。如要進一步瞭解詳細資訊,請參閱 MDN 文章

功能和限制

結構化複製可以解決許多 (但並非所有) 技術的 JSON.stringify() 技術不足之處。結構化複製功能可處理週期性資料結構、支援多種內建資料類型,而且通常更可靠且通常更快可靠。

然而,此模式仍有一些限制,您可能無法免受應對:

  • 原型:如果您搭配類別執行個體使用 structuredClone(),您將收到純物件做為傳回值,因為結構化複製功能會捨棄物件的原型鏈。
  • 函式:如果物件包含函式,structuredClone() 會擲回 DataCloneError 例外狀況。
  • 不可複製:部分值無法複製,其中最常見的是 Error 和 DOM 節點。這會導致 structuredClone() 擲回。

如果您的用途因為上述任一限制會導致交易中斷,Lodash 等程式庫仍會提供自訂實作的其他深層連結演算法,但這些演算法不一定符合您的用途。

效能

雖然我未進行新的微型基準比較,但我曾在 2018 年初進行了比較,現在 structuredClone() 曝光之前便已廣泛採用。在這之前,JSON.parse() 是非常小物件的最快選項。應該不會有任何變動對大型物件而言,仰賴結構化複製的技術能夠更快 (顯著)。考慮到新的 structuredClone() 無須濫用其他 API,而且功能比 JSON.parse() 更強大,因此建議您將這個新方法設為預設建立深層連結。

結論

如果您需要在 JS 中建立值的深副本,也許是因為您使用不可變更的資料結構,或是想確保函式在不影響原始原始設定的情況下,處理物件,而無需再取得變通方法或程式庫。JS 生態系統現在有 structuredClone()。太厲害了。