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


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

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

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


4548
2018-06-06 14:59


起源


Eval不是邪恶的。使用eval很差。如果你害怕它的副作用,你使用它是错误的。您担心的副作用是使用它的原因。顺便提一下,有没有人回答你的问题? - Prospero
克隆对象是一件棘手的事情,特别是对于任意集合的自定义对象。这可能就是为什么没有开箱即用的方式来做到这一点。 - b01
eval() 通常是一个坏主意,因为 在处理通过设置的变量时,许多Javascript引擎的优化器必须关闭 eval。只是拥有 eval() 在您的代码中可能会导致性能下降。 - user568458
可能重复 最优雅的方法来克隆JavaScript对象 - John Slegers
这是最常见类型的克隆对象之间的性能比较: jsben.ch/#/t917Z - EscapeNetscape


答案:


注意: 这是对另一个答案的回复,而不是对这个问题的正确回答。如果您希望快速克隆对象,请关注 Corban在答案中的建议 对这个问题。


我想要注意的是 .clone() 方法 jQuery的 只克隆DOM元素。为了克隆JavaScript对象,您可以:

// Shallow copy
var newObject = jQuery.extend({}, oldObject);

// Deep copy
var newObject = jQuery.extend(true, {}, oldObject);

更多信息可以在 jQuery文档

我还要注意,深层副本实际上比上面显示的更加智能 - 它可以避免许多陷阱(例如,尝试深度扩展DOM元素)。它经常在jQuery核心和插件中使用,效果很好。


4067



对于那些没有意识到的人,John Resig的回答可能是作为一种回应/澄清 ConroyP的答案而不是直接回答问题。 - S. Kirby
@ThiefMaster github.com/jquery/jquery/blob/master/src/core.js 在第276行(有一些代码可以做其他事情,但“如何在JS中执行此操作”的代码是:) - Rune FS
这是jQuery深层拷贝背后的JS代码,对于任何感兴趣的人: github.com/jquery/jquery/blob/master/src/core.js#L265-327 - Alex W
哇!只是要非常明确:不知道为什么这个回答被选为正确答案,这是对以下答复的答复: stackoverflow.com/a/122190/6524 (推荐 .clone(),这不是在这种情况下使用的正确代码)。不幸的是,这个问题已经经历了如此多的修改,原来的讨论已经不再明显了!如果您关心速度,请按照Corban的建议并编写循环或将属性直接复制到新对象。或者亲自测试一下! - John Resig
如果不使用jQuery,如何做到这一点? - Awesomeness01


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

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

JSON.parse(JSON.stringify(obj))

成为深度克隆对象的最快方法(它击败了 jQuery.extend 深旗设置为10-20%)。

当深度标志设置为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通过创建一个新函数来执行本机克隆几乎就是 800X 比使用JSON.stringify要慢得多,这一切都非常快。


1877



@trysis Object.create不是克隆对象,正在使用原型对象... jsfiddle.net/rahpuser/yufzc1jt/2 - rahpuser
这个方法也会删除 keys 从你的 object, 其中有 functions 作为他们的价值观,因为 JSON 不支持功能。 - Karlen Kishmiryan
还要记住使用 JSON.parse(JSON.stringify(obj)) on Date Objects也会将日期转换回 世界标准时间 在字符串表示中 ISO8601 格式。 - dnlgmzddr
JSON方法也对循环引用产生了阻碍。 - rich remer
@ develop,Object.assign({},objToClone)看起来好像是一个浅层克隆 - 在开发工具控制台中使用它时,对象克隆仍然指向克隆对象的引用。所以我认为这不适用于此。 - Garrett Simpson


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

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

402



我刚刚发现这种方法的结果是如果你的对象有任何函数(我的内部有getter和setter),那么这些就会在字符串化时丢失..如果这就是你所需要的,这个方法很好.. - Markive
@Jason,这个方法比浅层复制(在深层对象上)慢的原因是这个方法,根据定义,深层复制。但是由于 JSON 在本机代码中实现(在大多数浏览器中),这将比使用任何其他基于javascript的深度复制解决方案快得多,并且可能 有时 比基于javascript的浅层复制技术更快(参见: jsperf.com/cloning-an-object/79)。 - MiJyn
JSON.stringify({key: undefined}) //=> "{}" - Web_Designer
这种技术也会破坏所有 Date 存储在对象内的对象,将它们转换为字符串形式。 - fstab
它将无法复制任何不属于JSON规范的内容(json.org) - cdmckay


结构化克隆

HTML5定义 内部“结构化”克隆算法 这可以创建对象的深层克隆。它仍然局限于某些内置类型,但除了JSON支持的几种类型外,它还支持日期,RegExps,地图,集合,Blob,FileLists,ImageDatas,稀疏数组, 键入的数组,将来可能更多。它还保留了克隆数据中的引用,允许它支持会导致JSON错误的循环和递归结构。

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

浏览器目前不提供结构化克隆算法的直接接口,而是全局的 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();


274



@rynah我只是仔细查看了规范而你是对的: history.pushState() 和 history.replaceState() 方法同步设置 history.state 到他们的第一个参数的结构化克隆。有点奇怪,但它的工作原理。我现在正在更新我的答案。 - Jeremy
这是错的!该API并不意味着以这种方式使用。 - Fardin
作为在Firefox中实现pushState的人,我觉得这个黑客的骄傲和反感奇怪。做的好各位。 - Justin L.


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

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


273



JQuery解决方案适用于DOM元素,但不适用于任何Object。 Mootools具有相同的限制。希望他们对任何对象都有一个通用的“克隆”......递归解决方案应该适用于任何事情。这可能是要走的路。 - jschrab
如果要克隆的对象具有需要参数的构造函数,则此函数会中断。看起来我们可以将它改为“var temp = new Object()”并让它在每种情况下都有效,不是吗? - Andrew Arnott
安德鲁,如果将其更改为var temp = new Object(),那么您的克隆将不会具有与原始对象相同的原型。尝试使用:'var newProto = function(){}; newProto.prototype = obj.constructor; var temp = new newProto();' - limscoder
与limscoder的答案类似,请参阅下面的答案,了解如何在不调用构造函数的情况下执行此操作: stackoverflow.com/a/13333781/560114 - Matt Browne
对于包含对子部分(即对象网络)的引用的对象,这不起作用:如果两个引用指向同一个子对象,则该副本包含两个不同的副本。如果有递归引用,函数将永远不会终止(好吧,至少不是你想要它的方式:-)对于这些一般情况,你必须添加一个已经复制的对象的字典,并检查你是否已经复制它...当您使用简单的语言时,编程很复杂 - virtualnobi


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

一个 Object.assign method是ECMAScript 2015(ES6)标准的一部分,完全符合您的要求。

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

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

阅读更多...

填充工具 支持旧浏览器:

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

132



这不会递归复制,因此无法真正提供克隆对象问题的解决方案。 - mwhite
这种方法很有效,虽然我测试了一些并且_.extend({},(obj))是最快的BY FAR:比JSON.parse快20倍,比Object.assign快60%。它可以很好地复制所有子对象。 - Nico
@mwhite克隆和深度克隆之间存在差异。这个答案确实克隆了,但它没有深度克隆。 - Meirion Hughes
op要求深度克隆。这不做深刻的克隆。 - user566245
哇这么多的Object.assign响应,甚至没有阅读op的问题...... - martin8768


码:

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

91



关于什么 var obj = {} 和 obj.a = obj - neaumusic
我不明白这个功能。假设 from.constructor 是 Date 例如。第三个怎么样? if 第2次测试到达 if 测试会成功并导致函数返回(因为 Date != Object && Date != Array)? - Adam McKee
@AdamMcKee因为 javascript参数传递和变量赋值很棘手。这种方法效果很好,包括日期(确实由第二次测试处理) - 在这里测试小提琴: jsfiddle.net/zqv9q9c6。 - brichins
*我的意思是“参数赋值”(在函数体内),而不是“变量赋值”。虽然它有助于彻底理解这两者。 - brichins
@NickSweeting:试试 - 可能有效。如果没有 - 修复它并更新答案。这就是它在社区中的工作方式:) - Kamarey


这就是我正在使用的:

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

81



这似乎不对。 cloneObject({ name: null }) => {"name":{}} - Niyaz
这是由于javascript中的另一个愚蠢的事情 typeof null > "object" 但 Object.keys(null) > TypeError: Requested keys of a value that is not an object. 把条件改为 if(typeof(obj[i])=="object" && obj[i]!=null) - Vitim.us
这将分配继承的可枚举属性 OBJ 直接到克隆,并假设 OBJ 是一个普通的对象。 - RobG
这也会混淆数组,它会转换为带有数字键的对象。 - blade
如果不使用null,则不成问题。 - Jorge Bucaran


按性能深度复制: 排名从最好到最差

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

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

当数组包含数字和字符串时 - 函数如.slice(),. concat(),. splice(),赋值运算符“=”和Underscore.js的克隆函数;将制作数组元素的深层副本。

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

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具有更好的性能:


64



我测试了一些,_.extend({},(obj))是最快的BY FAR:比JSON.parse快20倍,比Object.assign快60%。它可以很好地复制所有子对象。 - Nico
@NicoDurand - 您的在线性能测试吗? - tfmontague
你的所有例子都很浅,一层。这不是一个好的答案。该问题是关于 深 克隆即至少两个水平。 - Karl Morrison
深度复制是指在不使用对其他对象的引用指针的情况下完整复制对象的时间。 “深度复制对象数组”部分下的技术,例如jQuery.extend()和自定义函数(递归),复制具有“至少两个级别”的对象。所以,并非所有的例子都是“一级”副本。 - tfmontague
我喜欢你的自定义复制函数,但你应该排除空值,否则所有空值都被转换为对象,即: out[key] = (typeof v === "object" && v !== null) ? copy(v) : v; - josi


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

56