一尘不染

在 JavaScript 中深度克隆对象的最有效方法是什么?

javascript

我做过类似obj = JSON.parse(JSON.stringify(o));但质疑效率的事情。

我还看到了具有各种缺陷的递归复制函数。
我很惊讶不存在规范的解决方案。


阅读 244

收藏
2022-01-06

共2个答案

一尘不染

Native deep cloning

现在有一个名为structured cloning的 JS 标准,它在 Node 11 及更高版本中进行实验性工作,将登陆浏览器,并且具有适用于现有系统的 polyfill

structuredClone(value)

如果需要,首先加载 polyfill:

import structuredClone from '@ungap/structured-clone';

较旧的答案

数据丢失的快速克隆 - JSON.parse/stringify

如果不使用DateS,功能,undefinedInfinity,正则表达式,地图,集合,斑点,的文件列表,ImageDatas,稀疏数组,类型化数组或其他复杂类型的对象中,一个很简单的一个衬垫深克隆的对象是:

JSON.parse(JSON.stringify(object))
const a = {
  string: 'string',
  number: 123,
  bool: false,
  nul: null,
  date: new Date(),  // stringified
  undef: undefined,  // lost
  inf: Infinity,  // forced to 'null'
  re: /.*/,  // lost
}
console.log(a);
console.log(typeof a.date);  // Date object
const clone = JSON.parse(JSON.stringify(a));
console.log(clone);
console.log(typeof clone.date);  // result of .toISOString()

使用library进行可靠的克隆

由于克隆对象并非易事(复杂类型、循环引用、函数等),大多数主要库都提供了克隆对象的函数。不要重新发明轮子- 如果您已经在使用库,请检查它是否具有对象克隆功能。例如,

ES6(shallow copy)

为了完整起见,请注意 ES6 提供了两种浅拷贝机制:Object.assign()spread syntax. 。它将所有可枚举属性的值从一个对象复制到另一个对象。例如:

var A1 = {a: "2"};
var A2 = Object.assign({}, A1);
var A3 = {...A1};  // Spread Syntax
2022-01-06
一尘不染

结构化克隆

2021更新:structuredClone全局函数即将登陆浏览器,Node.js的,和杰诺。

HTML 标准包括一个内部结构化克隆/序列化算法,可以创建对象的深层克隆。它仍然仅限于某些内置类型,但除了 JSON 支持的少数类型之外,它还支持日期、正则表达式、地图、集合、Blob、文件列表、图像数据、稀疏数组、类型化数组,以及将来可能更多. 它还保留克隆数据中的引用,使其支持可能导致 JSON 错误的循环和递归结构。

Node.js 中的支持:实验性

structuredClone全球功能将很快被Node.js的提供:

const clone = structuredClone(original);

在那之前:v8Node.js 中的模块当前(从 Node 11 开始)直接公开结构化序列化 API,但此功能仍标记为“实验性”,并且在未来版本中可能会更改或删除。如果您使用的是兼容版本,则克隆对象非常简单:

const v8 = require('v8');

const structuredClone = obj => {
  return v8.deserialize(v8.serialize(obj));
};

浏览器的直接支持:即将推出

structuredClone全局函数很快就会被所有主流浏览器提供(前面已经讨论WHATWG / HTML#793 GitHub上)。它看起来/看起来像这样:

const clone = structuredClone(original);

在发布之前,浏览器的结构化克隆实现只是间接公开。

异步解决方法:可用。

使用现有 API 创建结构化克隆的开销较低的方法是通过MessageChannels 的一个端口发布数据。另一个端口将发出一个message带有附加.data. 不幸的是,监听这些事件必然是异步的,而同步的替代方案不太实用。

class StructuredCloner {
  constructor() {
    this.pendingClones_ = new Map();
    this.nextKey_ = 0;

    const channel = new MessageChannel();
    this.inPort_ = channel.port1;
    this.outPort_ = channel.port2;

    this.outPort_.onmessage = ({data: {key, value}}) => {
      const resolve = this.pendingClones_.get(key);
      resolve(value);
      this.pendingClones_.delete(key);
    };
    this.outPort_.start();
  }

  cloneAsync(value) {
    return new Promise(resolve => {
      const key = this.nextKey_++;
      this.pendingClones_.set(key, resolve);
      this.inPort_.postMessage({key, value});
    });
  }
}

const structuredCloneAsync = window.structuredCloneAsync =
    StructuredCloner.prototype.cloneAsync.bind(new StructuredCloner);

使用示例:

const main = async () => {
  const original = { date: new Date(), number: Math.random() };
  original.self = original;

  const clone = await structuredCloneAsync(original);

  // They're different objects:
  console.assert(original !== clone);
  console.assert(original.date !== clone.date);

  // They're cyclical:
  console.assert(original.self === original);
  console.assert(clone.self === clone);

  // They contain equivalent values:
  console.assert(original.number === clone.number);
  console.assert(Number(original.date) === Number(clone.date));

  console.log("Assertions complete.");
};

main();

同步解决方法:太糟糕了!

同步创建结构化克隆没有好的选择。这里有一些不切实际的技巧。

history.pushState()并且history.replaceState()都创建了第一个参数的结构化克隆,并将该值分配给history.state. 您可以使用它来创建任何对象的结构化克隆,如下所示:

const structuredClone = obj => {
  const oldState = history.state;
  history.replaceState(obj, null);
  const clonedObj = history.state;
  history.replaceState(oldState, null);
  return clonedObj;
};

使用示例:

Hide code snippet

'use strict';

const main = () => {
  const original = { date: new Date(), number: Math.random() };
  original.self = original;

  const clone = structuredClone(original);

  // They're different objects:
  console.assert(original !== clone);
  console.assert(original.date !== clone.date);

  // They're cyclical:
  console.assert(original.self === original);
  console.assert(clone.self === clone);

  // They contain equivalent values:
  console.assert(original.number === clone.number);
  console.assert(Number(original.date) === Number(clone.date));

  console.log("Assertions complete.");
};

const structuredClone = obj => {
  const oldState = history.state;
  history.replaceState(obj, null);
  const clonedObj = history.state;
  history.replaceState(oldState, null);
  return clonedObj;
};

main();

虽然是同步的,但这可能会非常慢。它会产生与操纵浏览器历史记录相关的所有开销。重复调用此方法可能会导致 Chrome 暂时无响应。

Notification构造函数创建其相关数据的结构化克隆。它还尝试向用户显示浏览器通知,但除非您请求通知权限,否则这将静默失败。如果您有其他用途的许可,我们将立即关闭我们创建的通知。

const structuredClone = obj => {
  const n = new Notification('', {data: obj, silent: true});
  n.onshow = n.close.bind(n);
  return n.data;
};

使用示例:

Hide code snippet

'use strict';

const main = () => {
  const original = { date: new Date(), number: Math.random() };
  original.self = original;

  const clone = structuredClone(original);

  // They're different objects:
  console.assert(original !== clone);
  console.assert(original.date !== clone.date);

  // They're cyclical:
  console.assert(original.self === original);
  console.assert(clone.self === clone);

  // They contain equivalent values:
  console.assert(original.number === clone.number);
  console.assert(Number(original.date) === Number(clone.date));

  console.log("Assertions complete.");
};

const structuredClone = obj => {
  const n = new Notification('', {data: obj, silent: true});
  n.close();
  return n.data;
};

main();
2022-01-06