如何正确克隆 JavaScript 对象?

2022-08-29 21:47:01

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

如何正确克隆 JavaScript 对象?


答案 1

2022年更新

有一个新的JS标准叫做结构化克隆。它适用于所有浏览器:

const clone = structuredClone(object);

旧答案

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

除了不可枚举的属性之外,当您尝试复制具有隐藏属性的对象时,还会遇到更棘手的问题。例如,是函数的隐藏属性。此外,对象的原型使用属性引用,该属性也是隐藏的,并且不会被迭代源对象属性的for/in循环复制。我认为可能是特定于Firefox的JavaScript解释器的,在其他浏览器中可能是不同的东西,但你明白了。并非所有内容都是可枚举的。如果你知道一个隐藏属性的名字,你可以复制它,但我不知道有什么方法可以自动发现它。prototype__proto____proto__

然而,在寻求优雅解决方案的过程中,另一个障碍是正确设置原型继承的问题。如果源对象的原型是 ,则只需使用 创建一个新的常规对象即可工作,但如果源的原型是 的某个后代,那么您将缺少使用过滤器跳过的原型中的其他成员,或者原型中的其他成员,但首先不可枚举。一种解决方案可能是调用源对象的属性来获取初始复制对象,然后复制属性,但这样您仍然无法获得不可枚举的属性。例如,Date 对象将其数据存储为隐藏成员:Object{}ObjecthasOwnPropertyconstructor

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

的日期字符串将比 后 5 秒。使一个方法与另一个相同方法是调用该方法,但这是特定于该类的。我不认为这个问题有一个防弹的一般解决方案,尽管我很乐意犯错!d1d2DatesetTimeDate

当我必须实现一般的深度复制时,我最终妥协了,假设我只需要复制一个普通的,,,,,或。最后3种类型是不可变的,所以我可以执行浅层复制,而不必担心它的变化。我进一步假设包含在或中的任何元素也将是该列表中的6种简单类型之一。这可以通过如下所示的代码来完成:ObjectArrayDateStringNumberBooleanObjectArray

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对象,但只要你不假设它只适用于你扔给它的任何东西,它可能足以用于许多目的。


答案 2

如果在对象中不使用 s、函数、未定义、正则表达式或无穷大,则非常简单的一个衬里是:DateJSON.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'
}
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()

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

另请参阅这篇关于浏览器的结构化克隆算法的文章,该算法在向工作线程发布消息和从工作线程发布消息时使用。它还包含用于深度克隆的功能。