题 如何正确克隆JavaScript对象?


我有一个对象, x。我想把它复制为对象 y,这样改变 y 不要修改 x。我意识到复制从内置JavaScript对象派生的对象将导致额外的,不需要的属性。这不是问题,因为我正在复制我自己的一个文字构造的对象。

如何正确克隆JavaScript对象?


2403


起源


看到这个问题: stackoverflow.com/questions/122102/... - Niyaz
对于JSON,我使用 mObj=JSON.parse(JSON.stringify(jsonObject)); - Lord Loh.
我真的不明白为什么没有人建议 Object.create(o),它做了作者要求的一切吗? - froginvasion
var x = { deep: { key: 1 } }; var y = Object.create(x); x.deep.key = 2;  这样做之后 y.deep.key 也将是2,因此不能使用Object.create进行克隆... - Ruben Stolk
@ r3wt无法正常工作......请在完成解决方案的基本测试后发布。 - akshay


答案:


更新的答案

只是用 Object.assign() 如建议 这里

但请注意,这只是一个浅拷贝。嵌套对象仍被复制为引用。


过时的答案

为JavaScript中的任何对象执行此操作并不简单或直接。您将遇到错误地从对象原型中拾取属性的问题,该属性应保留在原型中而不会复制到新实例。例如,如果您要添加 clone 方法 Object.prototype,如某些答案所示,您需要明确跳过该属性。但是如果添加其他额外的方法会怎么样呢 Object.prototype,或其他你不了解的中间原型?在这种情况下,您将复制您不应该使用的属性,因此您需要使用。来检测无法预料的非本地属性 hasOwnProperty 方法。

除了不可枚举的属性,当您尝试复制具有隐藏属性的对象时,您将遇到更严峻的问题。例如, prototype 是函数的隐藏属性。此外,使用属性引用对象的原型 __proto__,也是隐藏的,并且不会被遍历源对象属性的for / in循环复制。我认为 __proto__ 可能是Firefox的JavaScript解释器特有的,它可能在其他浏览器中有所不同,但你得到了图片。并非一切都是可以计算的。如果您知道其名称,则可以复制隐藏属性,但我不知道有任何方法可以自动发现它。

寻求优雅解决方案的另一个障碍是正确设置原型继承的问题。如果您的源对象的原型是 Object,然后简单地创建一个新的通用对象 {} 会工作,但如果源的原型是一些后代 Object,那么你将会错过你使用该跳过的原型中的其他成员 hasOwnProperty 过滤器,或者原型中的过滤器,但首先不是可枚举的。一种解决方案可能是调用源对象 constructor 获取初始复制对象然后复制属性的属性,但是您仍然不会获得不可枚举的属性。例如,a Date object将其数据存储为隐藏成员:

function clone(obj) {
    if (null == obj || "object" != typeof obj) return obj;
    var copy = obj.constructor();
    for (var attr in obj) {
        if (obj.hasOwnProperty(attr)) copy[attr] = obj[attr];
    }
    return copy;
}

var d1 = new Date();

/* Executes function after 5 seconds. */
setTimeout(function(){
    var d2 = clone(d1);
    alert("d1 = " + d1.toString() + "\nd2 = " + d2.toString());
}, 5000);

日期字符串 d1 将落后5秒 d2。一种制作方法 Date 与另一个相同的是通过调用 setTime 方法,但具体到 Date 类。我不认为这个问题有一个防弹的一般解决方案,但我会很高兴出错!

当我不得不实施一般的深度复制时,我最终通过假设我只需复制一个普通版来妥协 ObjectArrayDateStringNumber, 要么 Boolean。最后3种类型是不可变的,所以我可以执行浅拷贝而不用担心它会改变。我进一步假设包含的任何元素 Object 要么 Array 也将是该列表中的6种简单类型之一。这可以通过以下代码完成:

function clone(obj) {
    var copy;

    // Handle the 3 simple types, and null or undefined
    if (null == obj || "object" != typeof obj) return obj;

    // Handle Date
    if (obj instanceof Date) {
        copy = new Date();
        copy.setTime(obj.getTime());
        return copy;
    }

    // Handle Array
    if (obj instanceof Array) {
        copy = [];
        for (var i = 0, len = obj.length; i < len; i++) {
            copy[i] = clone(obj[i]);
        }
        return copy;
    }

    // Handle Object
    if (obj instanceof Object) {
        copy = {};
        for (var attr in obj) {
            if (obj.hasOwnProperty(attr)) copy[attr] = clone(obj[attr]);
        }
        return copy;
    }

    throw new Error("Unable to copy obj! Its type isn't supported.");
}

只要对象和数组中的数据形成树结构,上述函数就可以适用于我提到的6种简单类型。也就是说,对象中的相同数据的引用不超过一个。例如:

// This would be cloneable:
var tree = {
    "left"  : { "left" : null, "right" : null, "data" : 3 },
    "right" : null,
    "data"  : 8
};

// This would kind-of work, but you would get 2 copies of the 
// inner node instead of 2 references to the same copy
var directedAcylicGraph = {
    "left"  : { "left" : null, "right" : null, "data" : 3 },
    "data"  : 8
};
directedAcyclicGraph["right"] = directedAcyclicGraph["left"];

// Cloning this would cause a stack overflow due to infinite recursion:
var cyclicGraph = {
    "left"  : { "left" : null, "right" : null, "data" : 3 },
    "data"  : 8
};
cyclicGraph["right"] = cyclicGraph;

它将无法处理任何JavaScript对象,但它可能足以用于许多目的,只要您不认为它只适用于您抛出的任何内容。


1313



几乎 在nodejs中运行良好 - 只需更改(var i = 0,var len = obj.length; i <len; ++ i){for for(var i = 0; i <obj.length; +) + i){ - Trindaz
对于未来的googlers:相同的深层副本,递归传递引用而不是使用'return'语句 gist.github.com/2234277 - Trindaz
如今 JSON.parse(JSON.stringify([some object]),[some revirer function]) 是一个解决方案? - KooiInc
在第一个片段中,您确定它不应该是 var cpy = new obj.constructor()? - cyon
对象分配似乎没有在Chrome中创建真正的副本,维护对原始对象的引用 - 最终使用JSON.stringify和JSON.parse进行克隆 - 完美地工作 - 1owk3y


使用jQuery,你可以 浅拷贝 同 延伸

var copiedObject = jQuery.extend({}, originalObject)

对copiedObject的后续更改不会影响originalObject,反之亦然。

或者做一个 深刻的副本

var copiedObject = jQuery.extend(true, {}, originalObject)

713



甚至: var copiedObject = jQuery.extend({},originalObject); - Grant McLean
将true指定为深拷贝的第一个参数也很有用: jQuery.extend(true, {}, originalObject); - Will Shaver
是的,我发现此链接很有用(与Pascal相同的解决方案) stackoverflow.com/questions/122102/... - Garry English
@Will Shaver - 是的!而已!没有深拷贝选项它对我不起作用! - thorinkor
只是一个注释,这不复制 原 原始对象的构造函数 - Sam Jones


如果您不在对象中使用函数,则可以使用以下非常简单的内容:

var cloneOfA = JSON.parse(JSON.stringify(a));

这适用于包含对象,数组,字符串,布尔值和数字的所有类型的对象。

也可以看看 本文关于浏览器的结构化克隆算法 在向工作人员发送消息和从工作人员发布消息时使用。它还包含深度克隆功能。


692



请注意,这只能用于测试。首先,它在时间和内存消耗方面远非最佳。其次,并非所有浏览器都有此方法。 - Nux
@Nux,为什么不在时间和记忆方面达到最佳? MiJyn说:“这种方法比浅层复制(在深层对象上)慢的原因在于,根据定义,这种方法是深层复制。但由于JSON是在本机代码中实现的(在大多数浏览器中),因此速度会快得多比使用任何其他基于javascript的深度复制解决方案,有时可能比基于javascript的浅层复制技术更快(参见:jsperf.com/cloning-an-object/79)。“ stackoverflow.com/questions/122102/... - BeauCielBleu
我只想在2014年10月为此添加更新。使用JSON.parse(JSON.stringify(oldObject))可以更快地使用Chrome 37+;使用它的好处是,如果需要,javascript引擎很容易看到并优化为更好的东西。 - mirhagk
如果对象具有不可字符串化的东西,例如,它会废弃整个JSON 无穷, 未定义等。试试这个对象: a = { b: Infinity, c: undefined } - kumar_harsh
2016年更新: 这应该适用于几乎所有被广泛使用的浏览器。 (看到 我可以用吗...)现在的主要问题是它是否足够高效。 - James Foster


在ECMAScript 6中有 Object.assign 方法,它将所有可枚举的自有属性的值从一个对象复制到另一个对象。例如:

var x = {myProp: "value"};
var y = Object.assign({}, x); 

但请注意,嵌套对象仍会被复制为引用。


509



是的,我相信 Object.assign 是要走的路。它也很容易填充: gist.github.com/rafaelrinaldi/43813e707970bd2d77fa - Rafael
但请注意,这只是一个浅拷贝。嵌套对象仍被复制为引用! - ohager
还要注意,这将复制通过对象文字定义的“方法”(因为这些 是可枚举的)但是 不 方法通过“类”机制藐视(因为这些 不 枚举)。 - Marcus Junius Brutus
我想应该提到的是,除了Edge之外,IE并没有支持。有些人仍然使用这个。 - Saulius
这与@EugeneTiurin的答案相同。 - Wilt


答案很多,但没有提到 的Object.create 来自ECMAScript 5,它肯定不会给你一个精确的副本,但将源设置为新对象的原型。

因此,这不是问题的确切答案,但它是一个单行解决方案,因而优雅。它适用于2种情况:

  1. 这种继承是有用的(呃!)
  2. 不会修改源对象的位置,从而使两个对象之间的关系成为非问题。

例:

var foo = { a : 1 };
var bar = Object.create(foo);
foo.a; // 1
bar.a; // 1
foo.a = 2;
bar.a; // 2 - prototype changed
bar.a = 3;
foo.a; // Still 2, since setting bar.a makes it an "own" property

为什么我认为这个解决方案更优越?它是原生的,因此没有循环,没有递归。但是,旧版浏览器需要使用polyfill。


114



注意:Object.create不是深层副本(itpastorn没有提到递归)。证明: var a = {b:'hello',c:{d:'world'}}, b = Object.create(a); a == b /* false */; a.c == b.c /* true */; - zamnuts
这是原型继承,而不是克隆。这些是完全不同的东西。新对象没有任何属性,只是指向原型的属性。克隆的目的是创建一个不引用另一个对象中任何属性的新对象。 - d13
我完全赞成你。我也同意这不是克隆,因为可能是“打算”。但是,遇到人,接受JavaScript的本质,而不是试图找到不标准化的模糊解决方案。当然,你不喜欢原型,它们对你来说都是“啰嗦”,但如果你知道你在做什么,它们实际上非常有用。 - froginvasion
@RobG:本文解释了引用和克隆之间的区别: en.wikipedia.org/wiki/Cloning_(programming)。 Object.create 通过引用指向父级的属性。这意味着如果父级的属性值发生更改,则子级也将更改。这有一些令人惊讶的副作用,嵌套数组和对象可能会导致代码中难以发现的错误,如果你不知道它们: jsbin.com/EKivInO/2。克隆对象是一个全新的独立对象,它具有与父对象相同的属性和值,但未连接到父对象。 - d13
这会误导人们... Object.create()可以用作继承手段,但克隆远不及它。 - prajnavantha


在一行代码中克隆Javascript对象的优雅方法

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

105



抱歉这个愚蠢的问题,但为什么呢 Object.assign 当时有两个参数 value polyfill中的函数只需要一个参数? - Qwertie
@Qwertie昨天所有参数都被迭代并合并到一个对象中,优先考虑上次传递的arg中的属性 - Eugene Tiurin
哦,我明白了,谢谢(我不熟悉 arguments 对象之前。)我找不到 Object() 通过谷歌...它是一个类型转换,不是吗? - Qwertie
这只会执行一个浅薄的“克隆” - Marcus Junius Brutus


MDN

  • 如果你想要浅拷贝,请使用 Object.assign({}, a)
  • 对于“深度”复制,请使用 JSON.parse(JSON.stringify(a))

不需要外部库,但您需要检查 首先是浏览器兼容


105



JSON.parse(JSON.stringify(a))看起来很漂亮,但在使用它之前,我建议您对所需的集合进行基准测试。根据对象大小,这可能不是最快的选项。 - Edza
我注意到JSON方法将日期对象转换为字符串但不返回日期。必须在Javascript中处理时区的乐趣并手动修复任何日期。除日期外,其他类型可能类似 - Steve Seeger
对于Date,我会使用moment.js,因为它有 clone 功能。在这里看到更多 momentjs.com/docs/#/parsing/moment-clone - Tareq