JavaScript 物件單元 ── 結構化複製(structuredClone)
簡介
在日常開發中,我們常常需要把一個物件或資料結構「完整」地複製一份,避免在原本的資料上不小心做出副作用。傳統的做法是使用 JSON.stringify / JSON.parse、Object.assign 或是展開運算子(...),但這些方法都有明顯的限制:無法正確處理循環參照、Date、RegExp、Map、Set、Blob、File 等特殊類型。
ECMAScript 2021(ES12)正式將 structuredClone 引入標準,讓開發者可以以原生且安全的方式,深度複製任何支援「結構化序列化」的值。它的行為與瀏覽器內建的 postMessage 所使用的序列化機制相同,且在 Node.js 17 以上也已支援。掌握 structuredClone,能讓你在資料搬移、狀態管理、測試 mock 等情境下,寫出更可靠且維護成本更低的程式碼。
核心概念
1. 什麼是「結構化複製」?
structuredClone(value, [options]) 會對 value 進行「結構化序列化」再反序列化,產生一個 深層且獨立 的副本。它支援的類型包括:
| 類型 | 支援情況 |
|---|---|
| 原始值(Number、String、Boolean、null、undefined、Symbol) | ✅ |
| 物件、陣列 | ✅ |
| Date、RegExp、Map、Set、ArrayBuffer、TypedArray | ✅ |
| Blob、File、ImageBitmap、OffscreenCanvas | ✅ |
| 循環參照(Circular reference) | ✅ |
不支援:Function、Error(除非是 DOMException)、BigInt (在某些環境)、DOM Node(除非是 ImageBitmap) |
❌ |
注意:
structuredClone會拋出DataCloneError(或DOMException)如果傳入的值無法被序列化。
2. 基本語法
const copy = structuredClone(originalValue[, options]);
originalValue:要被複製的資料。options(可選):目前只有transfer屬性,可用於「轉移」ArrayBuffer、MessagePort、ImageBitmap等可轉移物件,類似postMessage的行為。
// 只轉移而不複製(原始物件會變成 neutered)
const [buffer, transferred] = structuredClone({ buf }, { transfer: [buf] });
3. 為什麼它比 JSON.parse(JSON.stringify(...)) 更好?
| 項目 | JSON 方法 |
structuredClone |
|---|---|---|
| 循環參照 | ❌ 會拋錯 | ✅ |
| Date、RegExp | 變成字串 | ✅ 保留原型 |
| Map、Set | 失真 | ✅ 完整保留 |
| Binary(ArrayBuffer) | 失真或需手動處理 | ✅ 自動處理 |
| 性能 | 常因字串化而較慢 | 多數情況下更快(原生實作) |
程式碼範例
範例 1:基本深層複製
const person = {
name: 'Alice',
hobbies: ['reading', 'hiking'],
profile: {
age: 28,
created: new Date('1996-07-15')
}
};
const clone = structuredClone(person);
// 修改 clone 不會影響原本
clone.profile.age = 30;
clone.hobbies.push('coding');
console.log(person.profile.age); // 28
console.log(person.hobbies); // ['reading', 'hiking']
說明:
clone與person完全分離,即使是巢狀物件或陣列也不會共享參考。
範例 2:支援 Map、Set、RegExp
const data = {
map: new Map([['key1', 1], ['key2', 2]]),
set: new Set([1, 2, 3]),
regex: /ab+c/gi
};
const copy = structuredClone(data);
console.log(copy.map instanceof Map); // true
console.log(copy.set instanceof Set); // true
console.log(copy.regex instanceof RegExp); // true
// 確認內容相同
console.log([...copy.map]); // [['key1',1],['key2',2]]
console.log([...copy.set]); // [1,2,3]
說明:
Map、Set、RegExp都被正確保留其型別與內容。
範例 3:處理循環參照
const node = { name: 'node' };
node.self = node; // 循環參照
const clonedNode = structuredClone(node);
console.log(clonedNode === clonedNode.self); // true
console.log(clonedNode === node); // false
說明:即使資料結構中出現自我參照,
structuredClone仍能正確重建相同的循環結構。
範例 4:轉移(Transfer)ArrayBuffer
// 建立一個 8 位元的緩衝區
const buffer = new Uint8Array([1, 2, 3, 4]).buffer;
// 使用 transfer 讓 buffer 直接被「搬移」而不是複製
const [transferred] = structuredClone({ buf: buffer }, { transfer: [buffer] });
console.log(buffer.byteLength); // 0 (已被 neutered)
console.log(transferred.buf.byteLength); // 4
說明:在需要大量資料搬移(如 Web Worker)時,使用
transfer可以避免不必要的記憶體拷貝。
範例 5:在 Node.js 中複製 Blob
// Node.js 18+ 才支援 Blob
const blob = new Blob(['Hello, world!'], { type: 'text/plain' });
(async () => {
const clone = structuredClone(blob);
const text = await clone.text();
console.log(text); // "Hello, world!"
})();
說明:
Blob也是可被structuredClone處理的物件,適用於檔案上傳、緩衝區傳遞等情境。
常見陷阱與最佳實踐
| 陷阱 | 描述 | 解決方式 |
|---|---|---|
| Function 無法被克隆 | structuredClone 會拋出 DataCloneError。 |
若必須保留函式,改用手動複製或將函式抽離成模組、重新注入。 |
| Error 物件會失真 | Error 只會保留 message,其他屬性會遺失。 |
只在需要傳遞錯誤訊息時使用;若要完整保留,自行序列化(JSON.stringify(error, ...))或建立自訂錯誤類別。 |
| BigInt 在舊環境不支援 | 某些瀏覽器或 Node 版本仍未支援 BigInt 的結構化克隆。 | 確認執行環境(if (typeof structuredClone === 'function')),或將 BigInt 轉成字串後再克隆。 |
| 轉移物件被「neutered」 | 使用 transfer 後,原始緩衝區會變成長度 0。 |
僅在確定不再使用原始緩衝區時才使用;若仍需保留,改用深層複製。 |
| 跨執行環境限制 | Web Worker、Service Worker、Node Worker 之間的傳遞需要支援同樣的類型。 | 事先確認目標環境支援的類型清單,避免在不支援的環境拋錯。 |
最佳實踐
先檢查支援性
if (typeof structuredClone !== 'function') { throw new Error('此瀏覽器不支援 structuredClone,請使用 polyfill 或其他方法。'); }盡量避免在熱路徑大量使用
雖然structuredClone為原生實作,仍會涉及記憶體分配。對於每幀更新的遊戲或即時渲染,應慎用或先做效能測試。配合 TypeScript 時明確型別
const copy = structuredClone(original) as typeof original;使用
transfer以提升跨執行緒效能
只在需要大量二進位資料(如音訊、影像)時使用,能顯著降低 GC 壓力。在測試環境中利用
structuredClone產生獨立測試資料
透過深層複製避免測試案例之間互相污染。
實際應用場景
| 場景 | 為什麼選擇 structuredClone |
|---|---|
| Redux / Zustand 狀態快照 | 想要在「時間旅行除錯」時取得完整的狀態快照,且包含 Map、Set、Date 等非純 JSON 類型。 |
| Web Worker 與主執行緒資料傳遞 | 直接使用 postMessage(data) 背後即是 structuredClone,若想先在主執行緒驗證資料,可手動呼叫 structuredClone。 |
| 伺服器端渲染 (SSR) 中的資料深拷貝 | 在 Node.js 中,把從資料庫取得的物件傳遞給多個渲染流程時,避免共享同一參考。 |
| 單元測試的 mock 資料 | 測試函式是否會意外改變傳入的物件;使用 structuredClone 輕鬆產生不會被測試程式修改的基礎資料。 |
| 圖形或音訊處理的緩衝區搬移 | 大型 ArrayBuffer 或 AudioBuffer 在 Web Worker 中處理時,使用 transfer 讓資料「零拷貝」搬移。 |
總結
structuredClone 為 JavaScript 帶來了 安全、完整且原生的深層複製 能力。相較於傳統的 JSON 方案或手動遞迴複製,它不僅支援更廣泛的資料型別,還能正確處理循環參照與大型二進位緩衝區的「轉移」操作。對於 狀態管理、跨執行緒通訊、測試資料準備 等常見需求,structuredClone 都是值得優先考慮的工具。
在實務開發中,記得:
- ✅ 確認執行環境支援(或提供 polyfill)
- ✅ 留意不支援的類型(Function、Error 等)
- ✅ 盡可能使用
transfer以提升跨執行緒效能 - ✅ 在效能敏感的路徑上做基準測試
掌握這項新功能,將讓你的 JavaScript 程式碼更 健全、可維護,也能在日益複雜的前端與後端應用中,保持資料的正確與一致。祝你在開發旅程中玩得開心、寫得順手!