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

「平台」現在隨附了結構化複製功能,「structuredClone()」。

您過去必須先仰賴因應措施和程式庫,才能建立 JavaScript 值的深層副本,平台現在隨附 structuredClone(),這是內建的深度複製函式。

瀏覽器支援

  • Chrome:98。
  • Edge:98。
  • Firefox:94。
  • Safari: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 中,原始值 (原始值、原始資料類型) 是不是物件且沒有方法的資料。原始資料類型有七種:字串、數字、bigint、布林值、未定義、符號和空值。

MDN - 基本

非原始值會當成參照處理,也就是說,複製值的行為實際上只是複製同一基礎物件的參照,進而產生淺層複製行為。

深度複製

淺層文案的正向是深層文案。深度複製演算法也會逐一複製物件的屬性,但在找到另一個物件的參照時,以遞迴方式叫用本身,進而建立該物件的副本。這可能十分重要,可確保兩個程式碼片段不會意外共用物件,並在不知情的情況下修改彼此的狀態。

以往在 JavaScript 中建立值的深層,沒有任何簡單或好的方法。許多人仰賴第三方程式庫,例如 Lodash 的 cloneDeep() 函式。這個問題最常見的解決方法是 JSON 型駭客:

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

事實上,這是很受歡迎的替代方案,其中 V8 會積極最佳化 JSON.parse(),特別是上述模式,盡可能加快速度。它的速度很快,但也有一些缺點和三線:

  • 遞迴資料結構:當您提供遞迴資料結構時,系統會擲回 JSON.stringify()。處理已連結的清單或樹狀結構時,很容易發生這種情況。
  • 內建類型:如果值包含 MapSetDateRegExpArrayBuffer 等其他 JS 內建項目,系統會擲回 JSON.stringify()
  • 函式JSON.stringify() 會靜靜地捨棄函式。

結構化複製

平台必須在幾個地方建立 JavaScript 值的深度副本:在 IndexedDB 儲存 JS 值需要某種形式的序列化程序,才能儲存在磁碟中,之後經過反序列化才能還原 JS 值。同樣地,透過 postMessage() 傳送訊息至 WebWorker 需要將 JS 值從一個 JS 領域轉移到另一個 JS 領域。這類演算法稱為「結構化本機副本」,但截至最近為止,開發人員不易使用這種演算法。

但現在不一樣了!HTML 規格已修改為公開名為 structuredClone() 的函式,這個函式會執行該演算法做為方法,讓開發人員輕鬆建立 JavaScript 值的深度副本。

const myDeepCopy = structuredClone(myOriginal);

這樣就大功告成了!這是整個 API如要瞭解詳情,請參閱 MDN 文章

功能與限制

結構化複製可以解決 JSON.stringify() 技巧的許多缺點。結構化複製功能可以處理週期性資料結構,支援許多內建資料類型,而且通常更可靠且速度更快。

不過,這項策略仍有一些限制,可能會讓您主動出擊:

  • Prototypes:如果您將 structuredClone() 與類別例項搭配使用,您會傳回純物件做為傳回值 值,因為結構化複製會捨棄物件的原型鏈。
  • 函式:如果物件含有函式,structuredClone() 會擲回 DataCloneError 例外狀況。
  • 不可複製:部分值無法結構化複製,最值得注意的是 Error 和 DOM 節點。這項服務 就會擲回 structuredClone()

如果這些限制會影響您的用途,Lodash 等程式庫仍會提供自訂實作的其他深度複製演算法,以滿足您的使用需求。

成效

雖然我從未進行新的微型基準比較,但我在 2018 年初進行過比較,發現 structuredClone() 出現前。在當時,JSON.parse() 是極小物件的最快選項。我期望它維持不變。在較大的物件中,使用結構化複製功能的技術 (明顯) 會更快。由於新的 structuredClone() 不會濫用其他 API 的負擔,而且比 JSON.parse() 更強大,建議您將其設為建立深度副本的預設方法。

結論

如果您需要在 JS 中建立值的深度副本,可能是因為使用不可變更的資料結構,或想確保函式可以在不影響原始物件的情況下操控物件,而您不再需要尋求解決方法或程式庫。JS 生態系統現在有 structuredClone()。太厲害了!