js实现深拷贝原生方法(译文)

340 阅读4分钟

你知道javascript中有原生方法可以实现Object的深拷贝嘛?

它来了, structuredClone 函数被加入了js运行时:

const calendarEvent = {
  title: "Builder.io Conf",
  date: new Date(123),
  attendees: ["Steve"]
}

// 😍
const copied = structuredClone(calendarEvent)

你发现我们通过上述方法不仅复制了对象, 还有嵌套数组, 甚至是Date对象?

所有工作都完全符合预期:

copied.attendees // ["Steve"]
copied.date // Date: Wed Dec 31 1969 16:00:00
cocalendarEvent.attendees === copied.attendees // false
// 译者也尝试过了,这些确实是深拷贝

structuredClone 不仅可以复制上述结构,还可以:

  • 复制无限嵌套的Object和Array
  • 复制循环引用
  • 复制各种各样的js数据类型,例如: DateSetMapErrorRegExpArrayBufferBlobFileImageData, and many more
  • 移动(transfer)任何 transferable objects
    译者注:transferable objects在transfer时将自己的内存移动给新的对象,自己不可再使用这块内存,可以避免分配不必要的内存

image.png

再举个例子,以下这一坨也可以完美拷贝:

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!')
}
kitchenSink.circular = kitchenSink

// ✅ All good, fully and deeply copied!
const clonedSink = structuredClone(kitchenSink)

为什么不用对象扩展?

必须要表明,我们现在在探讨深拷贝. 如果你只需要做浅层拷贝,不包括嵌套对象和数据, 那我们就可以这样做:

const simpleEvent = {
  title: "Builder.io Conf",
}
// ✅ 没问题,这没有嵌套数组和对象
const shallowCopy = {...calendarEvent}

或者这样

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

但是一旦遇到嵌套数据和对象,那么就会出现问题

const calendarEvent = {
  title: "Builder.io Conf",
  date: new Date(123),
  attendees: ["Steve"]
}

const shallowCopy = {...calendarEvent}

// 🚩 oops - 这样会在原对象和新对象的attendees数据都加上Bob
shallowCopy.attendees.push("Bob")

// 🚩 oops - 会同时印象原对象和新对象上的date
shallowCopy.date.setTime(456)

所以,这并不是完整的深拷贝.

嵌套的Date和数组仍然是使用指针引用实现的, 如果我们想更改这些对象的话就会出现问题.

为什么不使用Json.parse(Json.stringify(x))

确实这个小把戏有时候挺好用的,并且表现惊人, 但是和structuredClone 相比存在一些缺点.

举个例子:

const calendarEvent = {
  title: "Builder.io Conf",
  date: new Date(123),
  attendees: ["Steve"]
}

// 🚩 JSON.stringify 会将Date变成string
const problematicCopy = JSON.parse(JSON.stringify(calendarEvent))

查看控制台,可以得到

{
  title: "Builder.io Conf",
  date: "1970-01-01T00:00:00.123Z"
  attendees: ["Steve"]
}

这不是我们想要的! date 应该是 Date 对象, 而不是string.

这是因为JSON.stringify 只能作用于基础对象(basic objects), arrays, 和 primitives. 其他的类型都会使用不可预期的方法实现. 例如, Dates 会被转化为 string. 但是 Set 会被转化为 {}.

JSON.stringify 甚至会直接忽略掉一些内容, 例如 undefined 或者 functions.

再举个例子, 如果我们使用这种方法拷贝 kitchenSink:

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

将会得到

{
  "set": {},
  "map": {},
  "regex": {},
  "deep": {
    "array": [
      {}
    ]
  },
  "error": {},
}

译者注,其实会得到这个:

image.png

并且我们必须删除循环引用, 因为JSON.stringify 会直接报错.

因此,虽然如果我们的需求符合它所能做的,这种方法可能很好,而对于上面无法实现的情况,structuredClone 都可以完美的实现.

为什么不使用.cloneDeep()?

现在, Lodash中的 cloneDeep 方法是对深拷贝非常通用的解决方案.

事实上,它确实名副其实:

import cloneDeep from 'lodash/cloneDeep'

const calendarEvent = {
  title: "Builder.io Conf",
  date: new Date(123),
  attendees: ["Steve"]
}

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

只有一个美中不足的地方. 就是import的消耗 Import Cost , 这个函数引入增加了17.4kb 的大小 (gzipped压缩后5.3kb):

image.png 假如你只想引入这个方法. 如果你使用更为常见的方式import, 没有意识到这种方法将导致不会进行tree shaking, 你实际上增加了25kb 仅仅为了这个函数(译者:这个数据纯纯为了吓唬人,这能过review?) 😱

image.png

structuredClone不能复制什么

函数不能复制

会产生DataCloneError 报错:

// 🚩 Error!
structuredClone({ fn: () => { } })

Dom节点不能复制

// 🚩 Error!
structuredClone({ el: document.body })

descriptors, setters, and getters不能复制

给个例子自己品:

structuredClone({ get foo() { return 'bar' } })
// Becomes: { foo: 'bar' }

对象的原型(property)不能复制

原型链不能进行复制. 如果你复制 MyClass, 复制得到的结果将不被认为是一个class (但是所有有效的属性(vaild properties)将被复制)

class MyClass { 
  foo = 'bar' 
  myMethod() { /* ... */ }
}
const myClass = new MyClass()

const cloned = structuredClone(myClass)
// Becomes: { foo: 'bar' }
// 译者:最终得到结果就是{ foo: 'bar' }对象,已经不是class了
cloned instanceof myClass // false

所有可以复制的数据类型

More simply put, anything not in the below list cannot be cloned:

JS Built-ins

ArrayArrayBufferBooleanDataViewDateErrorMap , Object 但仅限于普通对象(例如,来自对象文本), 基本数据类型Primitive types, 除了 symbol ( numberstringnullundefinedbooleanBigInt), RegExpSetTypedArray

Error types

ErrorEvalErrorRangeErrorReferenceError , SyntaxErrorTypeErrorURIError

Web/API types

AudioDataBlobCryptoKeyDOMExceptionDOMMatrixDOMMatrixReadOnlyDOMPointDomQuadDomRectFileFileListFileSystemDirectoryHandleFileSystemFileHandleFileSystemHandleImageBitmapImageDataRTCCertificateVideoFrame

浏览器支持

structuredClone 被所有主流浏览器支持, 甚至包括Node.js 和 Deno.

只需要注意在Web Workers里使用时存在缺陷:

image.png

总结

很长一段时间了,但我们终于有了structuredClone,可以轻松地在JavaScript中进行深度克隆对象.Thank you, Surma.

原文出处: www.builder.io/blog/struct…