跳到主要內容
問題研究

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()

參考資料

分享這篇