structuredClone():終於有內建的深層拷貝
整理 JavaScript structuredClone() 的深層拷貝用法,說明 Date、Map、Set、循環參考的處理方式,以及 Function、class 與 Vue reactive Proxy 的注意事項。
W
JavaScript 的 Object、Array 是參考型資料。直接指定只會複製參考,兩個變數仍然指向同一份資料:
const original = { name: 'Tom' }
const assigned = original
assigned.name = 'John'
console.log(original.name)
// 'John'
先分清楚淺層拷貝
展開運算子可以建立新的第一層物件,但巢狀物件仍然共用原本的參考:
const original = {
name: 'Tom',
profile: { age: 20 }
}
const shallow = { ...original }
shallow.profile.age = 30
console.log(original.profile.age)
// 30
所以 { ...object } 是淺層拷貝;需要讓巢狀資料也分開時,才是深層拷貝要處理的範圍。
以前想快速做深層拷貝,很常看到:
const clone = JSON.parse(JSON.stringify(original))
但這種方法遇到 Date、Map、Set、undefined 或循環參考時會有問題。
現在可以直接使用:
const clone = structuredClone(original)
structuredClone 能處理什麼
例如:
const original = {
date: new Date(),
map: new Map([['name', '小明']]),
set: new Set([1, 2, 3]),
nested: {
value: 123
}
}
const clone = structuredClone(original)
clone.date instanceof Date
// true
clone.map.get('name')
// '小明'
clone.set.has(2)
// true
它也可以處理循環參考:
const original = {
name: '小明'
}
original.self = original
const clone = structuredClone(original)
clone.self === clone
// true
這是 JSON.parse(JSON.stringify()) 做不到的。
不能複製的東西
structuredClone() 不是所有 JavaScript 物件都能複製。
例如函式:
structuredClone({
fn: () => {}
})
// DataCloneError
DOM 節點也不行:
structuredClone(document.body)
// DataCloneError
如果資料裡包含不能複製的內容,它會直接丟出 DataCloneError。
class 不會保留原型
例如:
class User {
constructor(name) {
this.name = name
}
greet() {
return `Hi ${this.name}`
}
}
const user = new User('小明')
const clone = structuredClone(user)
資料還在:
clone.name
// '小明'
但它不再是原本的 User:
clone instanceof User
// false
clone.greet()
// TypeError
所以 structuredClone() 比較適合複製「資料」,不是複製帶有行為的 class instance。
Vue reactive 的注意事項
Vue 的 reactive() 回傳的是 Proxy。
因此不能直接:
const state = reactive({
name: '小明',
items: []
})
structuredClone(state)
// DataCloneError
可以先使用 toRaw() 取得原始物件:
import { toRaw } from 'vue'
const snapshot = structuredClone(
toRaw(state)
)
如果使用 ref():
const form = ref({
name: '',
items: []
})
const snapshot = structuredClone(
toRaw(form.value)
)
這很適合用在表單編輯,例如先保存一份原始資料:
const original = structuredClone(
toRaw(form.value)
)
取消編輯時就可以拿這份資料還原。
需要注意,如果物件內部本來就另外塞入其他 reactive() 建立的 Proxy,單純 toRaw() 最外層仍可能不夠。
和 JSON 深拷貝的差異
| 資料 | JSON 方法 | structuredClone |
|---|---|---|
| 一般 Object / Array | 可以 | 可以 |
Date | 變字串 | 保留 |
Map / Set | 無法正常保留 | 保留 |
undefined | 可能消失 | 保留 |
NaN / Infinity | 變 null | 保留 |
| 循環參考 | Error | 可以 |
| Function | 消失或無法處理 | DataCloneError |
| class 原型 | 不保留 | 不保留 |
| Vue Proxy | 無法正常使用 | DataCloneError |
什麼時候使用
一般資料要做深層拷貝時,可以優先考慮:
const clone = structuredClone(original)
例如:
- 複製 API 回傳資料
- 保存表單修改前的狀態
- 複製巢狀 Object / Array
- 資料包含
Date、Map、Set - 資料可能有循環參考
如果物件包含 Function、特殊 class 行為或 Vue Proxy,就需要另外處理。
而:
JSON.parse(JSON.stringify(value))
比較適合真的要確認「JSON 序列化之後會長什麼樣子」的場合,而不是拿來當通用深層拷貝工具。
小結
現在需要複製一般 JavaScript 資料,可以先想到:
const clone = structuredClone(original)
它比:
JSON.parse(JSON.stringify(original))
能正確處理更多 JavaScript 資料型別。
但要記住三個限制:
Function → 不行
class prototype → 不保留
Vue reactive Proxy → 先 toRaw()