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

如何正确克隆JavaScript对象?


当前回答

要处理JSON.stringify无法处理的循环对象,可以引入一个名为JSOG的库,该库将任意图形序列化和反序列化为JSON格式。

var clone = JSOG.parse(JSOG.stringify(original));

尝试使用此技巧修补JSOG以进行克隆可能也很有趣(目前没有时间,但如果有人想尝试一下…):

序列化简单函数:

foo.f = function(a) { return a }
var stringForm = foo.f.toString() // "function (a) { return a }"

反序列化函数:

eval("foo.f = " + stringForm)

需要一些约定(可能是以属性的名称)来标识函数与正则字符串(可能是@func_f)。

当然,如果该函数调用第二个函数,则第二个功能将需要与原始函数一样存在。

然而,如果您要接受来自不可信来源的序列化表单,则上述内容非常危险,但接受来自不可靠来源的任何形式的函数都是危险的,因此如果您对克隆函数感兴趣,则必须已经建立了信任(或者您已经打算编写安全漏洞!)。

免责声明:我没有测试JSOG字符串/解析与JSON字符串/解析的速度,但它确实适用于我测试的简单(循环)对象。

其他回答

const objClone = { ...obj };

请注意,嵌套对象仍作为引用复制。

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对象,但只要您不认为它只适用于您向它抛出的任何东西,它就可以满足许多目的。

这里许多同行针对深度克隆提出的解决方案JSON.parse(JSON.stringify(orig_obj)有几个问题,我发现这些问题如下:

它在复制原始对象中未定义值的条目时丢弃这些条目,如果有一些值,如Infinity、NaN等,它们将在复制时转换为null,如果原始对象中存在Date类型,则它将在克隆对象中字符串化(typeof Date_entry-->string)。

找到了一种克隆对象的有效方法,它在各种场景中都很适合我。请看一看下面的代码,因为它已经解决了JSON.parse(…)的所有上述缺陷,但最终实现了正确的深度克隆:

var orig_obj = {
  string: 'my_str',
  number: 123,
  bool: false,
  nul: null,
  nested : {
    value : true
  },
  nan : NaN,
  date: new Date(), 
  undef: undefined,
  inf: Infinity,
}
console.log("original_obj before modification: ", orig_obj, "\n");
console.log(typeof orig_obj.date, "\n");

var clone_obj = Object.assign({}, orig_obj);

//this below loop will help in deep cloning and solving above issues
for(let prop in orig_obj) {
    if(typeof orig_obj[prop] === "object") {
        if(orig_obj[prop] instanceof Date)
            clone_obj[prop] = orig_obj[prop];
        else {
            clone_obj[prop] = JSON.parse(JSON.stringify(orig_obj[prop]));
        }
    }
}

console.log("cloned_obj before modification: ", orig_obj, "\n");

clone_obj.bool = true;
clone_obj.nested.value = "false";

console.log("original_obj post modification: ", orig_obj, "\n");
console.log("cloned_obj post modification: ", clone_obj, "\n");
console.log(typeof clone_obj.date);
function clone(src, deep) {

    var toString = Object.prototype.toString;
    if(!src && typeof src != "object"){
        //any non-object ( Boolean, String, Number ), null, undefined, NaN
        return src;
    }

    //Honor native/custom clone methods
    if(src.clone && toString.call(src.clone) == "[object Function]"){
        return src.clone(deep);
    }

    //DOM Elements
    if(src.nodeType && toString.call(src.cloneNode) == "[object Function]"){
        return src.cloneNode(deep);
    }

    //Date
    if(toString.call(src) == "[object Date]"){
        return new Date(src.getTime());
    }

    //RegExp
    if(toString.call(src) == "[object RegExp]"){
        return new RegExp(src);
    }

    //Function
    if(toString.call(src) == "[object Function]"){
        //Wrap in another method to make sure == is not true;
        //Note: Huge performance issue due to closures, comment this :)
        return (function(){
            src.apply(this, arguments);
        });

    }

    var ret, index;
    //Array
    if(toString.call(src) == "[object Array]"){
        //[].slice(0) would soft clone
        ret = src.slice();
        if(deep){
            index = ret.length;
            while(index--){
                ret[index] = clone(ret[index], true);
            }
        }
    }
    //Object
    else {
        ret = src.constructor ? new src.constructor() : {};
        for (var prop in src) {
            ret[prop] = deep
                ? clone(src[prop], true)
                : src[prop];
        }
    }

    return ret;
};

我最喜欢的优雅JS对象克隆解决方案是

function CloneObject() {}
function cloneObject(o) {
   CloneObject.prototype = o;
   return new CloneObject();
}

使用cloneObject(object)获取JS对象的克隆。

与许多复制解决方案不同,此克隆在克隆对象中保持原型关系。