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

如何正确克隆JavaScript对象?


当前回答

正如此链接所示,使用以下代码:

let clone = Object.create(Object.getPrototypeOf(obj),
 Object.getOwnPropertyDescriptors(obj));

其他回答

一个特别不优雅的解决方案是使用JSON编码对没有成员方法的对象进行深度复制。方法是对目标对象进行JSON编码,然后通过对其进行解码,获得所需的副本。您可以解码任意次数,以制作任意数量的副本。

当然,函数不属于JSON,因此这只适用于没有成员方法的对象。

这种方法非常适合我的用例,因为我将JSON blob存储在一个键值存储中,并且当它们在JavaScript API中作为对象公开时,每个对象实际上都包含对象的原始状态的副本,因此我们可以在调用方对公开的对象进行变异后计算增量。

var object1 = {key:"value"};
var object2 = object1;

object2 = JSON.stringify(object1);
object2 = JSON.parse(object2);

object2.key = "a change";
console.log(object1);// returns value

2022年更新

有一个新的JS标准叫做结构化克隆。它可以在许多浏览器中工作(请参阅我可以使用)。

const clone = structuredClone(object);

旧答案

对JavaScript中的任何对象执行此操作都不简单或直接。您将遇到错误地从对象的原型中提取属性的问题,这些属性应该留在原型中,而不是复制到新实例中。例如,如果您要向Object.prototype添加一个克隆方法,如某些答案所示,则需要显式跳过该属性。但是,如果有其他添加到Object.prototype或其他中间原型的方法,您不知道,该怎么办?在这种情况下,您将复制不应该复制的属性,因此需要使用hasOwnProperty方法检测不可预见的非本地属性。

除了非枚举属性之外,当您尝试复制具有隐藏财产的对象时,还会遇到更困难的问题。例如,原型是函数的隐藏属性。此外,对象的原型被__proto__属性引用,该属性也是隐藏的,不会被for/in循环复制到源对象的属性上。我认为__proto__可能特定于Firefox的JavaScript解释器,在其他浏览器中可能会有所不同,但你可以理解这一点。并非所有事物都是可枚举的。如果知道隐藏属性的名称,可以复制它,但我不知道有什么方法可以自动发现它。

寻求优雅解决方案的另一个障碍是正确设置原型继承的问题。如果源对象的原型是object,那么只需使用{}创建一个新的通用对象就可以了,但是如果源对象原型是object的后代,那么您将丢失该原型中使用hasOwnProperty筛选器跳过的其他成员,或者这些成员在原型中,但最初不可枚举。一种解决方案可能是调用源对象的构造函数属性以获取初始复制对象,然后复制属性,但这样仍然无法获取不可枚举的属性。例如,Date对象将其数据存储为隐藏成员:

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的日期字符串将比d2晚5秒。一种使一个Date与另一个Date相同的方法是调用setTime方法,但这是Date类特有的。我不认为这个问题有一个防弹的通用解决方案,尽管我很乐意错了!

当我不得不实现一般的深度复制时,我最终妥协了,假设我只需要复制一个普通的Object、Array、Date、String、Number或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对象,但只要您不认为它只适用于您向它抛出的任何东西,它就可以满足许多目的。

根据Apple JavaScript编码指南:

// Create an inner object with a variable x whose default
// value is 3.
function innerObj()
{
        this.x = 3;
}
innerObj.prototype.clone = function() {
    var temp = new innerObj();
    for (myvar in this) {
        // this object does not contain any objects, so
        // use the lightweight copy code.
        temp[myvar] = this[myvar];
    }
    return temp;
}

// Create an outer object with a variable y whose default
// value is 77.
function outerObj()
{
        // The outer object contains an inner object.  Allocate it here.
        this.inner = new innerObj();
        this.y = 77;
}
outerObj.prototype.clone = function() {
    var temp = new outerObj();
    for (myvar in this) {
        if (this[myvar].clone) {
            // This variable contains an object with a
            // clone operator.  Call it to create a copy.
            temp[myvar] = this[myvar].clone();
        } else {
            // This variable contains a scalar value,
            // a string value, or an object with no
            // clone function.  Assign it directly.
            temp[myvar] = this[myvar];
        }
    }
    return temp;
}

// Allocate an outer object and assign non-default values to variables in
// both the outer and inner objects.
outer = new outerObj;
outer.inner.x = 4;
outer.y = 16;

// Clone the outer object (which, in turn, clones the inner object).
newouter = outer.clone();

// Verify that both values were copied.
alert('inner x is '+newouter.inner.x); // prints 4
alert('y is '+newouter.y); // prints 16

史蒂夫

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

Object.assign方法是ECMAScript 2015(ES6)标准的一部分,它完全符合您的需要。

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

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

如果对象中没有循环依赖关系,我建议使用其他答案之一或jQuery的复制方法,因为它们看起来都很有效。

如果存在循环依赖关系(即,两个子对象彼此链接),那么你就有点糟糕了,因为(从理论角度)没有办法优雅地解决这个问题。