开发者问题收集

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

2008-09-23
2959994

克隆 JavaScript 对象的最有效方法是什么?我见过有人使用 obj = eval(uneval(o)); ,但 这是非标准的,并且仅受 Firefox 支持

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

我还见过存在各种缺陷的递归复制函数。
我很惊讶没有规范的解决方案。

3个回答

原生深度克隆

现在所有主流浏览器和 node >= 17 都支持 structuredClone(value) 函数。它具有 适用于旧系统的 polyfill

structuredClone(value)

如果需要,请先加载 polyfill:

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

参见 此答案 了解更多详情,但 请注意以下限制

  • 函数对象不能被结构化克隆算法复制;尝试抛出 DataCloneError 异常。
  • 克隆 DOM 节点同样会抛出 DataCloneError 异常。
  • 某些对象属性未保留:
    • RegExp 对象的 lastIndex 属性未保留。
    • 属性描述符、setter、getter 和类似元数据的功能不会被复制。例如,如果对象使用属性描述符标记为只读,则它将在副本中被读取/写入,因为这是默认设置。
    • 原型链不会被遍历或复制。

旧答案

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

如果您不使用 Date 、函数、 undefinedInfinity 、RegExp、Maps、Sets、Blob、FileLists、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()

参见 Corban 的答案 用于基准测试。

使用库进行可靠克隆

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

2008-09-23

查看此基准: http://jsben.ch/#/bWfk9

在我之前的测试中,速度是主要关注点,我发现

JSON.parse(JSON.stringify(obj))

是深度克隆对象最慢的方式(它比将 deep 标志设置为 true 的 jQuery.extend 慢 10-20%)。

deep 标志设置为 false (浅克隆)时,jQuery.extend 非常快。这是一个很好的选择,因为它包含一些额外的类型验证逻辑,并且不会复制未定义的属性等,但这也会使您的速度稍微变慢。

如果您知道您尝试克隆的对象的结构或者可以避免深度嵌套数组,您可以编写一个简单的 for (var i in obj) 循环来克隆您的对象,同时检查 hasOwnProperty,它会比 jQuery 快得多。

最后,如果您试图在热循环中克隆已知对象结构,您可以通过简单地内联克隆过程并手动构造对象来获得更高的性能。

JavaScript 跟踪引擎在优化 for..in 循环方面很糟糕,检查 hasOwnProperty 也会减慢您的速度。当速度是绝对必要条件时,请手动克隆。

var clonedObject = {
  knownProp: obj.knownProp,
  ..
}

请谨慎在 Date 对象上使用 JSON.parse(JSON.stringify(obj)) 方法 - JSON.stringify(new Date()) 返回 ISO 格式的日期字符串表示形式,而 JSON.parse() 不会 转换回 Date 对象。 有关更多详细信息,请参阅此答案

此外,请注意,至少在 Chrome 65 中,本机克隆不是可行的方法。据 JSPerf 称,通过创建新函数执行本机克隆比使用 JSON.stringify 慢近 800 倍 ,而 JSON.stringify 的速度在整个过程中都非常快。

针对 ES6 更新

如果您使用的是 Javascript ES6,请尝试使用此本机方法进行克隆或浅拷贝。

Object.assign({}, obj);
2011-03-17

结构化克隆

2022 年更新 structuredClone 全局函数 已在 Firefox 94、Node 17 和 Deno 1.14 中可用

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

Node.js 中的支持:

Node 17.0 提供了 structuredClone 全局函数

const clone = structuredClone(original);

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

const v8 = require('v8');

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

浏览器中的直接支持:在 Firefox 94 中可用

所有主流浏览器将很快提供 structuredClone 全局函数 (之前已在 GitHub 上的 whatwg/html#793 中讨论过)。它看起来/将看起来像这样:

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;
};

示例用法:

'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;
};

示例用法:

'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();
Jeremy
2012-06-06