大家好,很开心又碰面了,我是”后端高阶
怎样在 JavaScript 中复本两个第一类?对那个很简单的难题,但标准答案却不单纯。
1.提及fork
在 JavaScript 中大部份的小东西都是提及传达。假如你不晓得甚么原意,看一看上面的范例:
function mutate(obj) { obj.a = true; } const obj = {a: false}; mutate(obj) console.log(obj.a); // 输入 true表达式 mutate 发生改变了它的模块。在值传达的情景中,函数的实参而已std的两个复本——a copy——当表达式调用顺利完成后,并不发生改变std。但在 JavaScript 此种提及传达的情景中,表达式的实参和std对准同两个第一类,当模块外部发生改变实参的这时候,表达式外边的实参也被发生改变了。
因而在这类情况下,你须要留存原初第一类,此时你须要把原初第一类的两个复本传至到表达式中,以避免表达式发生改变原初第一类。
2.浅复本:Object.assign()
Object.assign(target, sources…)。它接受任意数量的源第一类,枚举它们的大部份属性并分配给target。假如我们使用两个新的空第一类target,那么我们就可以实现第一类的复制。
const obj = /* … */; const copy = Object.assign({}, obj);然而这而已两个浅复本。假如我们的第一类包含其它第一类作为自己的属性,它们将保持共享提及,这不是我们想要的:
function mutateDeepObject(obj) { obj.a.thing = true; } const obj = {a: {thing: false}}; constcopy =Object.assign({}, obj); mutateDeepObject(copy) console.log(obj.a.thing); // prints trueObject.assign 方法只会复本源第一类自身的并且可枚举的属性到目标第一类。该方法使用源第一类的[[Get]]和目标第一类的[[Set]],所以它会调用相关 getter 和 setter。因而,它分配属性,而不仅仅是复制或定义新的属性。假如合并源包含getter,这可能使其不适合将新属性合并到原型中。为了将属性定义(包括其可枚举性)复制到原型,应使用
Object.getOwnPropertyDescriptor()和Object.defineProperty() 。所以现在怎么办?有几种方法可以创建两个第一类的深复本。
注意:也许有人提到了第一类解构运算,这也是浅复本。
3.深复本的几种形式
3.1 JSON.parse
创建第一类复本的最古老方法之一是:将该第一类转换为其 JSON 字符串表示形式,然后将其解析回第一类。这感觉有点压抑,但它确实有效:
const obj = /* … */; const copy = JSON.parse(JSON.stringify(obj));这里的缺点是你创建两个临时的,可能很大的字符串,而已为了把它重新放回解析器。另两个缺点是此种方法不能处理循环第一类。而且循环第一类经常发生。例如,当您构建树状数据结构,其中两个节点提及其父级,而父级又提及其子级。
const x = {}; const y = {x}; x.y = y; // Cycle: x.y.x.y.x.y.x.y.x… const copy = JSON.parse(JSON.stringify(x)); // throws!另外,诸如 Map, Set, RegExp, Date, ArrayBuffer和其他内置类型在进行序列化时会丢失。
3.2 Structured Clone 结构化克隆算法
Structured cloning 是一种现有的算法,用于将值从两个地方转移到另一地方。例如,每当您调用postMessage将消息发送到另两个窗口或 WebWorker 时,都会使用它。关于结构化克隆的好处在于它处理循环第一类并全力支持大量的内置类型。难题是,在编写本文时,该算法并不能直接使用,只能作为其他 API 的一部分。
目前浏览器已经实现了structuredClone方法。可以阅读我的另一篇文章:
https://www.toutiao.com/article/7183471657846129211/3.3 MessageChannel
正如我所说的,只要你调用postMessage结构化克隆算法就可以使用。我们可以创建两个 MessageChannel 并发送消息。在接收端,消息包含我们原初数据第一类的结构化克隆。
function structuralClone(obj) { return new Promise(resolve => { const {port1, port2} = newMessageChannel(); port2.onmessage =ev => resolve(ev.data); port1.postMessage(obj); }); } const obj = /* … */; const clone = await structuralClone(obj);此种方法的缺点是它是异步的。虽然这并无大碍,但有这时候你须要使用同步的形式来深度复本两个第一类。
3.4 History API
假如你曾经使用history.pushState()写过 SPA,你就晓得你可以提供两个状态第一类来保存 URL。事实证明,那个状态第一类使用结构化克隆 – 而且是同步的。我们必须小心使用,不要把程序逻辑使用的状态对象搞乱了,所以我们须要在顺利完成克隆之后恢复原初状态。为了避免发生任何意外,请使用history.replaceState()而不是history.pushState()。
function structuralClone(obj) { const oldState = history.state; history.replaceState(obj, document.title); constcopy = history.state; history.replaceState(oldState,document.title); return copy; } const obj = /* … */; const clone = structuralClone(obj);然而,仅仅为了复制两个第一类,而使用浏览器的引擎,感觉有点过分。另外,Safari 浏览器对replaceState调用的限制数量为 30 秒内 100 次。
3.5 Notification API
在发了一条推文之后,Jeremy Banks 向我展示了第三种方法来利用结构化克隆:Notification API。
function structuralClone(obj) { return new Notification(, {data: obj, silent: true}).data; } const obj = /* … */; const clone = structuralClone(obj);短小,简洁。我喜欢它!
但,它须要浏览器外部的权限机制,所以我怀疑它是很慢的。由于某种原因,Safari 总是返回undefined。
4.Performance extravaganza
我想测量哪种方法是最高性能的。在我的第一次(天真的)尝试中,我拿了两个小 JSON 第一类,并通过不同的形式克隆第一类 1 千次。幸运的是,Mathias Bynens 告诉我,当你添加属性到两个第一类时,V8有两个缓存。所以我是在给缓存做基准测试。为了确保我永远不会碰到缓存,我编写了两个表达式,使用随机密钥名称生成给定深度和宽度的第一类,并重新运行测试。
以下是 Chrome,Firefox 和 Edge 中不同技术的性能。越低越好。
5.结论
那么我们从中得到了甚么呢?
假如您没有循环第一类,并且不须要留存内置类型,则可以使用跨浏览器的JSON.parse(JSON.stringify())获得最快的克隆性能,这让我感到非常惊讶。假如你想要两个适当的结构化克隆,MessageChannel是你唯一可靠的跨浏览器的选择。目前浏览器平台直接提供两个 structuredClone()表达式,具体参考文末资料。
参考资料
英文原文:Deep-copying in JavaScript – DasSur.ma
中文原文:
https://justjavac.com/javascript/2018/02/02/deep-copy.htmlhttps://www.toutiao.com/article/7183471657846129211/