查看原文
科技

【第3208期】用现代方式深度拷贝 JavaScript 中的对象

飘飘 前端早读课 2024-03-09

前言

介绍了 JavaScript 中的新功能 structuredClone,可以深度复制对象,包括嵌套数组和特殊类型。相比于其他方法如 JSON.parse (JSON.stringify (x)) 和 Lodash 的 cloneDeep,structuredClone 更全面且性能更好。今日前端早读课文章由 @飘飘翻译分享。

正文从这开始~~

你知道吗,JavaScript 中现在有一种原生方法可以对对象进行深度复制。

没错,这个 structuredClone 函数就是内置于 JavaScript 运行时中的:

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

// 😍
const copied = structuredClone(calendarEvent)

你是否注意到,在上面的示例中,我们不仅复制了对象,还复制了嵌套数组,甚至复制了Date对象?

【第2810期】JavaScript 深拷贝性能分析

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

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)

正如你所看到的,我们并没有完整复制这个对象。

嵌套的日期和数组仍然是两个对象之间的共享引用,如果我们要编辑这些引用,以为我们只是在更新复制的日历事件对象,这可能会给我们带来很大的麻烦。

【第1437期】深拷贝的终极探索

为什么不使用 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 应该是一个日期对象,而不是字符串。

出现这种情况是因为 JSON.stringify 只能处理基本对象、数组和基元。其他类型的处理方式很难预测。例如,日期会被转换为字符串。但集合会被简单地转换为 {}

JSON.stringify 甚至会完全忽略某些东西,比如未定义的或函数。

例如,如果我们用这个方法复制 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": {},
}

Ew!

哦,对了,我们还得删除原来为此设置的循环引用,因为 JSON.stringify 遇到循环引用时会直接抛出错误。

因此,如果我们的要求符合该方法的功能,那么该方法就会非常棒,但是我们可以使用 structuredClone(也就是上面我们没有做到的所有功能)来实现很多该方法无法实现的功能。

为什么不使用 _.cloneDeep

迄今为止,Lodash 的 cloneDeep 函数一直是解决这一问题的常用方法。

而事实上,它也确实能达到预期的效果:

import cloneDeep from 'lodash/cloneDeep'

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

const clonedEvent = cloneDeep(calendarEvent)

但有一点需要注意。我的集成开发环境中的 "导入成本" 扩展可以打印我导入任何东西的 kb 成本,根据该扩展,这个函数的最小化成本为 17.4kb(压缩后为 5.3kb):

这还只是假设你只导入了这个函数。如果你采用了更常见的导入方式,而没有意识到 "摇树" 并不总能如你所愿,那么你可能会因为这一个函数而意外导入多达 25kb 的数据 😱 。

虽然这对任何人来说都不会是世界末日,但在我们的情况下根本没有必要,因为浏览器已经内置了 structuredClone。

structuredClone 不能克隆什么

函数不能克隆

它们会引发 DataCloneError 异常:

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

也会引发 DataCloneError 异常:

// 🚩 Error!
structuredClone({ el: document.body })
属性描述符、设置器和获取器

以及类似元数据的功能不会被克隆。

例如,对于获取器,结果值会被克隆,但获取器函数本身(或任何其他属性元数据)不会被克隆:

structuredClone({ get foo() { return 'bar' } })
// 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 内置类型

数组、ArrayBuffer、布尔、DataView、日期、错误类型(下面特别列出的类型)、Map、对象(但仅限普通对象,例如来自对象字面的对象)、原始类型(符号除外)(又称数字、字符串、null、未定义、布尔、BigInt)、RegExp、Set、TypedArray

错误类型

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

网络 / API 类型

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

浏览器和运行时支持

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

需要注意的是,Web Worker 的支持比较有限:

结论

虽然姗姗来迟,但我们现在终于有了 structuredClone,可以轻而易举地在 JavaScript 中深度克隆对象了。谢谢你,Surma。

关于本文
译者:@飘飘
作者:@STEVE SEWELL
原文:https://www.builder.io/blog/structured-clone

这期前端早读课
对你有帮助,帮” 
 “一下,
期待下一期,帮”
 在看” 一下 。

继续滑动看下一个

【第3208期】用现代方式深度拷贝 JavaScript 中的对象

向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存