JavaScript中的深复制

在JavaScript中该如何复制一个对象?这是一个简单的问题,但答案却并不简单。

通过引用调用

JavaScript 通过传递各种值。下边是个例子:

function mutate(obj) {
  obj.a = true;
}

const obj = {a: false};
mutate(obj)
console.log(obj.a); // prints true

函数 mutate 改变了作为参数传递进来的对象。在“按值调用”的环境中,函数会传递该函数可以使用的值 - 所以是副本。 该函数对该对象所做的任何更改都不会在该函数外部可见。 但是在像 JavaScript 这样的“引用调用”环境中,函数会得到 - 引用,并且会改变实际的对象本身。 因此,最后的 console.log 将显示为 true

然而,有时候,您可能希望保留原始对象并为其他函数创建副本以便使用。

浅复制: Object.assign()

复制对象的一种方法是使用 `Object.assign()target,sources …)。 它需要任意数量的源对象,枚举它们自己的所有属性并将它们分配给目标。 如果我们使用一个新的空对象作为目标,我们基本上就做到了复制。

const obj = /* ... */;
const copy = Object.assign({}, obj);

但是,这仅仅是一个浅拷贝。如果我们的对象包含对象,它们将保持共享引用,这不是我们想要的:

function mutateDeepObject(obj) {
  obj.a.thing = true;
}

const obj = {a: {thing: false}};
const copy = Object.assign({}, obj);
mutateDeepObject(copy)
console.log(obj.a.thing); // prints true

另一件可能的事是 Object.assign()getter 变成简单的属性。 所以现在怎么办?原来,有几种方法可以创建对象的深层副本。

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!

此外,诸如Maps, Sets, RegExps, Dates, ArrayBuffers和其他内置类型的东西在序列化时会丢失。成为 String类型。

结构化克隆

结构化克隆是一种现有的算法,用于将值从一个领域转移到另一个领域。 例如,只要调用postMessage将消息发送到其他窗口或WebWorker,就会使用它。 关于结构化克隆的好处在于它处理循环对象并支持大量的内置类型。 问题在于,在编写本文时算法不会直接暴露,只能作为其他API的一部分。我想我们必须看看有哪些…

MessageChannel

正如我所说的,无论何时调用postMessage,都会使用结构化克隆算法。我们可以创建一个MessageChannel并发送消息。在接收端,消息包含我们原始数据对象的结构化克隆。

function structuralClone(obj) {
  return new Promise(resolve => {
    const {port1, port2} = new MessageChannel();
    port2.onmessage = ev => resolve(ev.data);
    port1.postMessage(obj);
  });
}

const obj = /* ... */;
const clone = await structuralClone(obj);

这种方法的缺点是它是异步的。这并不是什么大问题,但有时您需要一种同步方式来深度复制对象。

History API

如果您曾经使用 history.pushState() 构建SPA,那么您知道可以提供一个状态对象来保存URL。 事实证明,这个状态对象在结构上被克隆 - 同步。我们必须小心,不要混淆可能使用状态对象的任何程序逻辑,所以我们需要在完成克隆后恢复原始状态。 为了防止发生任何事件,请使用 history.replaceState() 而不是 history.pushState()

function structuralClone(obj) {
  const oldState = history.state;
  history.replaceState(obj, document.title);
  const copy = history.state;
  history.replaceState(oldState, document.title);
  return copy;
}

const obj = /* ... */;
const clone = structuralClone(obj);

再一次,为了复制一个对象而使用浏览器的引擎感觉有点过分,但是你必须做一些事情。此外,Safari会在30秒内将调用replaceState的次数限制为100次。

Notification API

通知API。通知有一个与它们相关的数据对象被克隆。

function structuralClone(obj) {
  return new Notification('', {data: obj, silent: true}).data;
}

const obj = /* ... */;
const clone = structuralClone(obj);
Written on March 2, 2018