1048 字
5 分鐘

structuredClone 深拷貝怎麼選:DataCloneError、class 與 transfer 限制

把 JSON.parse(JSON.stringify(value)) 換成 structuredClone(value),可以保留更多資料型別,卻不能把所有 JavaScript 物件原封不動複製。遇到 DataCloneError 或複製後的 format is not a function,先檢查資料裡是否混入函式、DOM 節點或自訂 class 行為。

structuredClone 適合建立可複製資料的獨立副本;需要保留物件行為時,應明確建立新實例。 transfer 則會讓原本的資源失效,不適合用在還要繼續讀取原資料的表單草稿。

深拷貝先確認你要保留什麼#

展開運算子 { ...value } 只建立第一層新物件,巢狀物件仍共用參照。只更新某個欄位時,可以沿修改路徑逐層建立副本,不必為整份狀態做深拷貝;但若目的是讓使用者任意編輯一份獨立草稿,則要先盤點其中的型別。

下面使用普通資料物件示範,將整段存成 .mjs 後以 Node.js 執行:

import assert from 'node:assert/strict';
const source = {
profile: { name: '小夏' },
createdAt: new Date('2026-10-04T00:00:00Z'),
settings: new Map([['theme', 'dark']]),
optional: undefined,
};
source.self = source;
const copy = structuredClone(source);
copy.profile.name = '草稿';
assert.equal(source.profile.name, '小夏');
assert.notEqual(copy.profile, source.profile);
assert.ok(copy.createdAt instanceof Date);
assert.equal(copy.createdAt.getTime(), source.createdAt.getTime());
assert.ok(copy.settings instanceof Map);
assert.equal(copy.settings.get('theme'), 'dark');
assert.ok(Object.hasOwn(copy, 'optional'));
assert.equal(copy.self, copy);
// JSON 無法直接序列化這份循環資料。
assert.throws(() => JSON.stringify(source), TypeError);
console.log('data clone checks passed');

MDN與 HTML Standard提供可複製/可轉移資料的規則。這裡檢查的是值、型別與參照關係;不能只看 console.log(copy) 外觀相似就判定成功。

DataCloneError 先找不屬於資料的成員#

任何一部分無法序列化,都可能讓整次複製失敗。函式、DOM 節點等不能直接用 structured clone 複製;若只是把 callback 與資料放進同一個物件,先拆開兩者,再複製真正需要的資料。

import assert from 'node:assert/strict';
assert.throws(
() => structuredClone({ name: '篩選器', onSave() {} }),
{ name: 'DataCloneError' },
);
const data = { name: '篩選器', selected: ['CSS'] };
const actions = { onSave: (value) => value.selected.length };
const draft = structuredClone(data);
assert.equal(actions.onSave(draft), 1);

不要 catch 後直接回傳原物件,也不要偷偷改用 JSON 複製。前者重新共用巢狀參照,後者可能改掉資料型別,呼叫端卻以為拿到同樣語意的副本。若資料不能通過複製,應回報失敗或使用已定義欄位的轉換函式。

class 看似複製成功,方法卻不在了#

自訂 class 的原型鏈與 private elements 不會被複製;property descriptor、getter/setter 的描述也不會原樣保留。這些限制見 structured clone algorithm。

import assert from 'node:assert/strict';
class Draft {
constructor(title) { this.title = title; }
display() { return '草稿:' + this.title; }
}
const original = new Draft('複製測試');
const plain = structuredClone(original);
assert.equal(plain.title, '複製測試');
assert.equal(plain instanceof Draft, false);
assert.equal(typeof plain.display, 'undefined');
// 明確定義還原邊界:這裡只允許 title。
const restored = new Draft(plain.title);
assert.equal(restored.display(), '草稿:複製測試');

只補上 prototype 不會自動恢復 private fields、constructor 驗證或外部資源。要保留業務行為,就把可儲存資料與實例建立流程分開。若來源含 getter,也別假設讀取過程完全沒有副作用;先轉成由已知欄位組成的資料物件,再決定是否複製。

transfer 會把原 buffer 交出去#

一般複製保留來源;transfer 是移交可轉移資源。對 ArrayBuffer 而言,來源會被 detach。適用情境是你確定原路徑不再使用該 buffer,例如交給後續處理流程;不是加速所有深拷貝的通用旗標。

import assert from 'node:assert/strict';
const source = new Uint8Array([10, 20, 30]);
const copied = structuredClone(source);
assert.equal(source.byteLength, 3);
assert.notEqual(copied.buffer, source.buffer);
const moved = structuredClone(source, { transfer: [source.buffer] });
assert.deepEqual([...moved], [10, 20, 30]);
assert.equal(source.byteLength, 0);
assert.equal(source.buffer.byteLength, 0);

上面四組例子已由本次維護流程在 Node.js v24.13.0 執行。沒有執行瀏覽器 DOM 節點、Web Worker 傳遞、框架 reactive proxy 或大型資料效能比較;那些情境要另外驗收,不能從小物件 assertion 推論速度與記憶體收益。

在會保留多份非同步結果的介面中,可搭配 Promise.allSettled 的部分失敗處理:先決定哪些結果要留下,再決定是否建立獨立副本。複製資料不能代替請求生命週期或狀態所有權的管理。

參考資料:

MDN:structuredClone()

MDN:The structured clone algorithm

WHATWG:Safe passing of structured data

structuredClone 深拷貝怎麼選:DataCloneError、class 與 transfer 限制
https://laplusda.com/posts/javascript-structuredclone-datacloneerror/
作者
Zero
發佈於
2026-10-04
許可協議
CC BY-NC-SA 4.0