使用 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} 會使用 擴散運算子,對 myOriginal 的 (可枚舉) 屬性進行疊代。它會使用屬性名稱和值,並逐一將這些屬性指派給新建立的空物件。因此,產生的物件形狀相同,但會複製屬性和值清單。系統也會複製這些值,但 JavaScript 會以不同方式處理所謂的原始值和非原始值。引述 MDN 的說法:

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

MDN - 原始

非基本值會以參照方式處理,也就是說,複製值的動作其實只是複製對同一個基礎物件的參照,因此會產生淺層複製行為。

深層複製

深層複製是淺層複製的反義。深層複製演算法也會逐一複製物件的屬性,但在找到對其他物件的參照時會遞迴叫用自身,並建立該物件的副本。這點非常重要,可確保兩段程式碼不會意外共用物件,並在不知情的情況下操控彼此的狀態。

過去,在 JavaScript 中建立值的深層複本並沒有簡單或方便的方法。許多人會使用第三方程式庫,例如 Lodash 的 cloneDeep() 函式。這項問題最常見的解決方法是使用 JSON 的駭客攻擊:

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

事實上,這是相當熱門的解決方法,V8 積極最佳化 JSON.parse(),特別是上述模式,以便盡可能加快速度。雖然速度快,但也有一些缺點和陷阱:

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

結構化複製

平台已具備在幾個位置建立 JavaScript 值的深層副本的功能:在 IndexedDB 中儲存 JS 值需要某種序列化形式,才能儲存在磁碟上,並在日後還原 JS 值。同樣地,透過 postMessage() 將訊息傳送至 WebWorker 時,需要將 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()。太棒了!