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

IT技术 javascript object clone
2021-01-06 21:35:37

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

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

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

6个回答

原生深度克隆

它被称为“结构化克隆”,在 Node 11 及更高版本中进行实验性工作,并有望登陆浏览器。有关更多详细信息,请参阅此答案

数据丢失的快速克隆 - 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()

有关基准测试,请参阅Corban 的回答

使用库进行可靠的克隆

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

ES6(拷贝)

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

var A1 = {a: "2"};
var A2 = Object.assign({}, A1);
var A3 = {...A1};  // Spread Syntax
@Ricardo 当然,您可以看到答案的历史记录,在我写下评论后,在“ES6”之后添加了“(浅拷贝)”。现在更清楚的是,这是一个浅拷贝。
2021-02-10 21:35:37
我使用了一个叫做真正快速深度克隆的库:github.com/davidmarkclements/rfdc 对我来说效果很好。
2021-02-12 21:35:37
谨防!var A = { b: [ { a: [ 1, 2, 3], b: [4, 5, 6], c: [7, 8, 9] } ] }; B = Object.assign( {}, A ); delete B.b[0].b;它还将修改对象 A !
2021-02-27 21:35:37
@Unicornist 是的,这就是 Object.assign 没有回答以下问题的原因:“在 JavaScript 中深度克隆对象的最有效方法是什么?”。所以至少它不应该被呈现为深度克隆的 ES6 解决方案。标题“ES6”具有误导性,至少应该改一下以反映这不是深度克隆方法。“肤浅”这个词很容易被忽视,很多人只是采用了他们在 Stack Overflow 中找到的最简单的解决方案,而没有阅读所有内容。依赖 Object.assign 进行对象克隆是危险的。因此,我的评论。
2021-03-05 21:35:37
@Gabriel Hautclocq 这是因为A.bB.b都指向内存中的同一个对象。如果A有一个具有非对象值(如数字或字符串)的属性,它将被正常复制。但是当一个包含对象值的属性被复制时,它是按引用复制的,而不是按值复制的。另外,请记住,Array 是 JS 中的对象。证明:typeof [] == 'object' && [] instanceof Array
2021-03-08 21:35:37

检查这个基准:http : //jsben.ch/#/bWfk9

在我之前的测试中,我发现速度是主要问题

JSON.parse(JSON.stringify(obj))

是深克隆最慢的方式的对象(它是慢于jQuery.extenddeep标志设置由10-20%真)。

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

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

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

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

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

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

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

ES6 更新

如果您使用 Javascript ES6,请尝试使用这种本地方法进行克隆或浅拷贝。

Object.assign({}, obj);
请注意,您的工作台中有两个错误:首先,它将一些浅克隆(lodash_.cloneObject.assign)与一些深克隆(JSON.parse(JSON.stringify()))进行了比较。其次,它对 lodash 说是“深度克隆”,但它进行的是浅层克隆。
2021-03-07 21:35:37

假设您的对象中只有变量而不是任何函数,您可以使用:

var newObject = JSON.parse(JSON.stringify(oldObject));
对象具有属性,而不是变量。;-)
2021-02-14 21:35:37
对于具有圆形属性的对象失败
2021-02-22 21:35:37
功能日期也是如此
2021-03-07 21:35:37

结构化克隆

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

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

Node.js 中的支持:实验性 🙂

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

const clone = structuredClone(original);

在那之前:v8Node.js 中module当前(从 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;
};

使用示例:

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

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

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

使用示例:

这简直是​​大错特错!该 API 不打算以这种方式使用。
2021-02-15 21:35:37
pushState 或 Notification hack 对某些对象类型(如 Function)不起作用
2021-02-18 21:35:37
有人会碰巧知道原生 v8 Structured Cloning 是否容易受到原型污染的影响吗?
2021-02-18 21:35:37
作为在 Firefox 中实现 pushState 的人,我对这个 hack 感到自豪和厌恶的奇怪混合。做的好各位。
2021-02-26 21:35:37
@ShishirArora 你说得对,我刚试过,它抛出一个“未捕获的 DOMException:无法克隆对象。” 对于 Notification hack 也是如此。
2021-03-06 21:35:37

如果没有任何内置的,你可以尝试:

function clone(obj) {
    if (obj === null || typeof (obj) !== 'object' || 'isActiveClone' in obj)
        return obj;

    if (obj instanceof Date)
        var temp = new obj.constructor(); //or new Date(obj);
    else
        var temp = obj.constructor();

    for (var key in obj) {
        if (Object.prototype.hasOwnProperty.call(obj, key)) {
            obj['isActiveClone'] = null;
            temp[key] = clone(obj[key]);
            delete obj['isActiveClone'];
        }
    }
    return temp;
}