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

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

我已经完成了obj = JSON.parse(JSON.stringify(o));但质疑效率。

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

答案

原生深克隆

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

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

如果您不使用Date ,函数, undefinedInfinity ,RegExps,Maps,Sets,Blobs,FileLists,ImageDatas,Sparse Arrays,Typed Arrays 或对象中的其他复杂类型,则可以使用一种非常简单的方法来深度克隆对象:

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()spread 语法 。它将所有可枚举的自身属性的值从一个对象复制到另一个对象。例如:

var A1 = {a: "2"};
var A2 = Object.assign({}, A1);

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

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

JSON.parse(JSON.stringify(obj))

是深度克隆对象的最慢方法(它比jQuery.extenddeep flag 将 true 设置为 10-20%慢)。

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

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

最后,如果您尝试在热循环中克隆已知的对象结构,则只需内联克隆过程并手动构造对象,即可获得更多的性能。

JavaScript 跟踪引擎在优化for..in循环中很for..in并且检查 hasOwnProperty 也会降低您的速度。绝对必要时必须手动克隆。

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

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

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

ES6 更新

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

Object.assign({}, obj);

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

var newObject = JSON.parse(JSON.stringify(oldObject));

结构化克隆

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

Node.js 中的支持:实验性

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

const v8 = require('v8');

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

浏览器中的直接支持:也许最终? 😐

浏览器当前不提供结构化克隆算法的直接接口,但是在 GitHub 上的 whatwg / html#793 中已经讨论了全局的structuredClone()函数。按照目前的建议,将其用于大多数目的将非常简单:

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();

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

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

在一行代码中克隆(而非深度克隆)对象的有效方法

Object.assign方法是 ECMAScript 2015(ES6)标准的一部分,可以完全满足您的需求。

var clone = Object.assign({}, obj);

Object.assign()方法用于将所有可枚举的自身属性的值从一个或多个源对象复制到目标对象。

阅读更多...

支持旧版浏览器的polyfill

if (!Object.assign) {
  Object.defineProperty(Object, 'assign', {
    enumerable: false,
    configurable: true,
    writable: true,
    value: function(target) {
      'use strict';
      if (target === undefined || target === null) {
        throw new TypeError('Cannot convert first argument to object');
      }

      var to = Object(target);
      for (var i = 1; i < arguments.length; i++) {
        var nextSource = arguments[i];
        if (nextSource === undefined || nextSource === null) {
          continue;
        }
        nextSource = Object(nextSource);

        var keysArray = Object.keys(nextSource);
        for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
          var nextKey = keysArray[nextIndex];
          var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
          if (desc !== undefined && desc.enumerable) {
            to[nextKey] = nextSource[nextKey];
          }
        }
      }
      return to;
    }
  });
}

码:

// extends 'from' object with members from 'to'. If 'to' is null, a deep clone of 'from' is returned
function extend(from, to)
{
    if (from == null || typeof from != "object") return from;
    if (from.constructor != Object && from.constructor != Array) return from;
    if (from.constructor == Date || from.constructor == RegExp || from.constructor == Function ||
        from.constructor == String || from.constructor == Number || from.constructor == Boolean)
        return new from.constructor(from);

    to = to || new from.constructor();

    for (var name in from)
    {
        to[name] = typeof to[name] == "undefined" ? extend(from[name], null) : to[name];
    }

    return to;
}

测试:

var obj =
{
    date: new Date(),
    func: function(q) { return 1 + q; },
    num: 123,
    text: "asdasd",
    array: [1, "asd"],
    regex: new RegExp(/aaa/i),
    subobj:
    {
        num: 234,
        text: "asdsaD"
    }
}

var clone = extend(obj);

这就是我正在使用的:

function cloneObject(obj) {
    var clone = {};
    for(var i in obj) {
        if(typeof(obj[i])=="object" && obj[i] != null)
            clone[i] = cloneObject(obj[i]);
        else
            clone[i] = obj[i];
    }
    return clone;
}

按性能进行深度复制:从最佳到最差

  • 重新分配 “=”(仅字符串数组,数字数组)
  • 切片(字符串数组,数字数组 - 仅)
  • 串联(仅字符串数组,数字数组)
  • 自定义功能:for 循环或递归复制
  • jQuery 的 $ .extend
  • JSON.parse(仅字符串数组,数字数组,对象数组)
  • Underscore.js的_.clone(字符串数组,若干阵列 - 只)
  • Lo-Dash 的_.cloneDeep

深度复制一个字符串或数字数组(一个级别 - 没有引用指针):

当数组包含数字和字符串时 - 诸如. slice()、. concat()、. splice()之类的函数,赋值运算符 “=” 和 Underscore.js 的 clone 函数;将复制该数组的元素。

重新分配表现最快的地方:

var arr1 = ['a', 'b', 'c'];
var arr2 = arr1;
arr1 = ['a', 'b', 'c'];

而且. slice()的性能优于. concat(), http: //jsperf.com/duplicate-array-slice-vs-concat/3

var arr1 = ['a', 'b', 'c'];  // Becomes arr1 = ['a', 'b', 'c']
var arr2a = arr1.slice(0);   // Becomes arr2a = ['a', 'b', 'c'] - deep copy
var arr2b = arr1.concat();   // Becomes arr2b = ['a', 'b', 'c'] - deep copy

深度复制对象数组(两个或多个级别 - 参考指针):

var arr1 = [{object:'a'}, {object:'b'}];

编写一个自定义函数(具有比 $ .extend()或 JSON.parse 更快的性能):

function copy(o) {
   var out, v, key;
   out = Array.isArray(o) ? [] : {};
   for (key in o) {
       v = o[key];
       out[key] = (typeof v === "object" && v !== null) ? copy(v) : v;
   }
   return out;
}

copy(arr1);

使用第三方实用程序功能:

$.extend(true, [], arr1); // Jquery Extend
JSON.parse(arr1);
_.cloneDeep(arr1); // Lo-dash

jQuery 的 $ .extend 具有更好的性能:

var clone = function() {
    var newObj = (this instanceof Array) ? [] : {};
    for (var i in this) {
        if (this[i] && typeof this[i] == "object") {
            newObj[i] = this[i].clone();
        }
        else
        {
            newObj[i] = this[i];
        }
    }
    return newObj;
}; 

Object.defineProperty( Object.prototype, "clone", {value: clone, enumerable: false});