likes
comments
collection
share

[译] JavaScript 实现对象深拷贝的新方式

作者站长头像
站长
· 阅读数 14

原作者: STEVE SEWELL

原文地址: www.builder.io/blog/struct…

[译] JavaScript 实现对象深拷贝的新方式

你知道吗, JavaScript 现在有一种原生方法可实现对象的深拷贝.

没错, 就是structuredClone函数, 它内置在 JavaScript 运行时.


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不仅能实现上述效果, 还能:

  • 拷贝无限嵌套对象和数组

  • 拷贝循环引用

  • 拷贝多种 JavaScript 类型, 如 Date, Set, Map, Error, RegExp, ArrayBuffer, Blob, File, ImageData等等

  • 转移可转移对象

下面的例子不出所料地实现了功能:


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'

}

// ✅ no problem, there are no nested objects or arrays

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 - we just added "Bob" to both the copy *and* the original event

shallowCopy.attendees.push('Bob')

// 🚩 oops - we just updated the date for the copy *and* original event

shallowCopy.date.setTime(456)

从上面的例子可以看出, 我们并没有彻底拷贝这个对象.

嵌套的日期和数组仍然是两者(在原始对象和拷贝后的对象之间)的共同引用. 如果我们想修改它俩时, 认为只是修改了拷贝后的calendarEvent对象, 这会导致重大问题.

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

没错, 这也是一个技巧. 这是一种和很好的方式, 效果也不错, 但有一些缺陷, 而structuredClone解决了这些缺陷.

举个例子:


const calendarEvent = {

title: 'Builder.io Conf',

date: new Date(123),

attendees: ['Steve']

}

// 🚩 JSON.stringify converted the `date` to a string

const problematicCopy = JSON.parse(JSON.stringify(calendarEvent))

打印problematicCopy得到


{

title: "Builder.io Conf",

date: "1970-01-01T00:00:00.123Z"

attendees: ["Steve"]

}

这不是我们想要的结果. date应该是一个Date对象, 而不是字符串.

因为JSON.stringify只能处理基本的对象, 数组和原始值. 对于其他类型, 很难预测是怎样处理的. 比如, Dates 会被转换为字符串. 但Set被简单地转为{}.

JSON.stringify甚至会完全忽略一些东西, 像undefined或函数.

例如, 如果拷贝 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": {},

}

哎呀!

另外, 必须移除原始值的循环引用, 因为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)

值得注意的是, 根据我的 IDE 中的 Import Cost 扩展可以看出引入包的大小, 仅这一个函数就有 17.4kb minified(5.3kb gzipped):

[译] JavaScript 实现对象深拷贝的新方式

这还是在假设你只导入了这一个函数. 如过你要导入更多通用方法, 没有意识到 tree shaking 可能会事与愿违, 你可能会为了这一个函数而无意中引入了 25kb😱

[译] JavaScript 实现对象深拷贝的新方式

structuredClone 不能拷贝什么

函数不能拷贝

会抛出 DataCloneError 异常:


// 🚩 Error!

structuredClone({ fn: () => {} })

DOM 节点

也会抛出 DataCloneError 异常:


// 🚩 Error!

structuredClone({ el: document.body })

属性描述符, setters, getters

同样, 类似元数据特征也不会被拷贝.

例如, 拷贝 getter 的结果是其返回值, 而不是 getter 函数本身(或者任意其他属性元数据)


structuredClone({

get foo() {

return 'bar'

},

set(n) {

console.log(n)

}

})

// Becomes: { foo: 'bar' }

对象原型

原型链不会被遍历或复制. 因此如果拷贝MyClass的一个实例, 拷贝后的对象不再是这个类的实例(但这个类的所有有效属性都会被克隆).


class MyClass {

foo = 'bar'

myMethod() {

/* ... */

}

}

const myClass = new MyClass()

const cloned = structuredClone(myClass)

// Becomes: { foo: 'bar' }

cloned instanceof MyClass // false

支持类型列表

简言之, 任何未在以下列表列出的, 都不能被拷贝.

JS 内置

Array, ArrayBuffer, Boolean, DataView, Date, Error 类型 (具体列举如下), Map , Object普通对象 (如对象字面量), 除了 symbol原始类型(即number, string, null, undefined, boolean, BigInt), RegExp, Set, TypedArray.

Error 类型

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

Web/API 类型

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

浏览器和运行时支持情况

最棒的是, 所有主流浏览器, 甚至 Node.js 和 Deno, 都支持structuredClone:

[译] JavaScript 实现对象深拷贝的新方式

Source: MDN

总结

日复一日, 我们终于有了structuredClone, 这让 JavaScript 可轻松实现对象深拷贝. 感谢 Surma.