在 JavaScript 中深度克隆(Deep Cloning)一个对象

现在,JavaScript 提供了一个底层的方法来深度克隆一个对象, 他就是 structureClone

const obj = {
   title: 'Object Title',
   date: new Date(),
   skills: ['php', 'javascript']
}

const copied = structuredClone(obj)

structureClone 不但可以复制对象,而且可以复制嵌套的数组,事件,时间对象。

copied.date
// Mon Feb 13 2023 23:40:53 GMT+0800 (中国标准时间)
copied.skills
// (2) ['php', 'javascript']

对的, structureClone 不但可以复制上面的内容,还可以作用于:

  1. 无限嵌套的对象和数组
  2. 循环引用
  3. 各种类型,包括 Date ,Set, Map, Error, RegExp, ArrayBuffer, Blob, File, ImageData 等等

为什么不用 对象展开?

重要的是深度复制,如果只是 浅层复制(shallow copy), 不需要复制嵌套对象或者数组,我们只需要做对象展开。

const simpleObject = {
    title: "Object Title",
    skills: ["php"]
}

const shallowCopy = {...simpleObject}

或者

const shallowCopy = Object.assign({}, simpleObject)
const shallowCopy = Object.create(simpleObject)

但是一旦有嵌套,就会有麻烦。

复制对象的修改会改变原来的对象。

const simpleObject = {
    title: "Object Title",
    skills: ["php"]
}

const shallowCopy = {...simpleObject}
// undefined
shallowCopy.skills.push("JS")
// 2
shallowCopy.skills
// (2) ['php', 'JS']
simpleObject.skills
// (2) ['php', 'JS']

可以看到,我们没有完全复制原来的对象。 嵌套的数组依然是原来引用地址。

用 JSON.parse(JSON.stringify(x)) ?

对的,这是个很好的窍门,而且非常高效。但有一些缺点,structuredClone 解决了。

例如:

const calendarEvent = {
  title: "Event Title",
  date: new Date(),
  attendees: ["Apple"]
}

// JSON.stringify 转换 `date` 成字符串
const problematicCopy = JSON.parse(JSON.stringify(calendarEvent))

// console.log
{
  title: "Event Title",
  date: "2023-02-14T08:10:46.676Z"
  attendees: ["Apple"]
}

这不是我们想要, date 应该是 Date 对象,而不是字符串。这是因为  JSON.stringify 只能处理基础对象,数组和基本类型。其他类型都将被转换,如时间变成字符串, Set 则简单的变成 {},甚至直接忽略 undefined 和 函数。

例如:

const kitchenSink = {
  set: new Set([1, 3, 3]),
  map: new Map([[1, 2]]),
  regex: /foo/,
  deep: { array: [ new File(someBlobData, 'file.txt') ] },
  error: new Error('Hello!')
}

const veryProblematicCopy = JSON.parse(JSON.stringify(kitchenSink))

// console.log(veryProblematicCopy )
{
  "set": {},
  "map": {},
  "regex": {},
  "deep": {
    "array": [
      {}
    ]
  },
  "error": {},
}

我们还必须删除原来的循环引用, 否则 JSON.stringify 会报错。

因此,如果我们的需求符合它可以做的事情,这个方法可能很好。

_.cloneDeep ?

Lodash 的 cloneDeep 函数称为一个通用的解决方案。结果也正如期望的那样。

import cloneDeep from 'lodash/cloneDeep'

const calendarEvent = {
  title: "Event Title",
  date: new Date(),
  attendees: ["Steve"]
}

// ✅ All good!
const clonedEvent = structuredClone(calendarEvent)

只有一个问题, 这个包 17.4k(gzipped: 5.3k)。

假设你只导入了这个函数。如果你没有意识到摇树优化并不总是像你希望的那样工作,而是以更常用的方式导入,那么你可能会不小心为这个函数导入25kb的文件。

如果浏览器已经内置了 structuredClone,我们就没有必要那么做。

什么不能被 structuredClone 克隆

函数不能被克隆。

会抛出异常 DataCloneError

DOM 节点 不能被克隆

也会抛出 DataCloneError

属性 descriptors, setters, and getters 不能被克隆

对象原型不能被克隆

所有支持的类型:

JS Built-ins

Array, ArrayBuffer, Boolean, DataView, Date, Error types (those specifically listed below), Map , Object but only plain objects (e.g. from object literals), Primitive types, except symbol (aka number, string, null, undefined, boolean, BigInt), RegExp, Set, TypedArray

Error types

Error, EvalError, RangeError, ReferenceError , SyntaxError, TypeError, URIError

Web/API types

AudioData, Blob, CryptoKey, DOMException, DOMMatrix, DOMMatrixReadOnly, DOMPoint, DomQuad, DomRect, File, FileList, FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemHandle, ImageBitmap, ImageData, RTCCertificate, VideoFrame