在JavaScript中完全克隆一个对象

21

我尝试在javascript中精确克隆一个对象。 我知道使用jquery的以下解决方案:

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

但是问题在于,对象的类型会丢失:

var MyClass = function(param1, param2) {
    alert(param1.a + param2.a);
};
var myObj = new MyClass({a: 1},{a: 2});
var myObjClone = jQuery.extend(true, {}, myObj);
alert(myObj instanceof MyClass);      // => true
alert(myObjClone instanceof MyClass); // => false

有没有什么解决方案可以在第二个弹框中得到true?


2
David,与所有其他克隆问题的不同之处在于,我询问如何保留对象类型属性。 - Tom
7个回答

12

jQuery.extend并不期望您使用instanceof运算符。它正在进行一个非常复杂的拷贝,而不是真正的克隆。仅仅循环遍历元素是不够的。此外,调用构造函数也不是最好的方法,因为您会失去参数。请尝试这样做:

var MyClass = function(param1, param2) {
    alert(param1.a + param2.a);
    this.p1 = param1;
    this.p2 = param2;
};

function Clone() { }
function clone(obj) {
    Clone.prototype = obj;
    return new Clone();
}

var myObj = new MyClass({a: 1},{a: 2});
var myObjClone = clone(myObj);
alert(myObj instanceof MyClass);      // => true
alert(myObjClone instanceof MyClass); // => true
console.log(myObj);       //note they are
console.log(myObjClone)   //exactly the same

请注意,由于您的原型现在指向原始对象(myObj),对myObj的任何更改都将反映在myObjClone中。Javascript的原型继承有些棘手。您需要确保新对象具有正确的原型,因此也具有正确的构造函数。

诚然,Javascript让我头疼。 不过,我认为我从ECMAScript语言规范中读到了正确的内容:

13.2.2 [[Construct]]
当调用一个可能带有参数的函数对象F的[[Construct]]内部方法时,执行以下步骤:

  1. 创建一个新的本地ECMAScript对象obj。
  2. 按照8.12中指定的方式设置obj的所有内部方法。
  3. 将obj的[[Class]]内部属性设置为"Object"。
  4. 将obj的[[Extensible]]内部属性设置为true。
  5. 使用参数">prototype"调用F的[[Get]]内部属性的值作为proto。
  6. 如果proto的类型是Object,则将obj的[[Prototype]]内部属性设置为proto。
  7. 如果proto的类型不是Object,则将obj的[[Prototype]]内部属性设置为15.2.4中描述的标准内置Object原型对象。
  8. 使用传递给[[Construct]]的参数列表args,将obj作为this值调用F的[[Call]]内部属性,并将结果赋给result。
  9. 如果result的类型是Object,则返回result。
  10. 返回obj。

这个人似乎比我更好地理解了这个概念。好的,我现在回到Java,那里我游得比沉多:)。


这是一个非常好的解决方案。此外,您可以将克隆体原型的所有属性复制到克隆体中,以便对myObj的更改不会反映在myObjClone中。但是,如果您再次使用不同的对象调用克隆,那么myObjClone的原型会发生变化吗? - Tom
如果你尝试使用"myNewObjClone = clone(myObj)",那么不,你并没有改变myObjClone的原型。Clone对象只存在于clone函数内部,因此每次调用clone(obj)时都会得到一个新的对象。这是使用闭包来“隐藏”变量的示例,在这种情况下,是对象Clone。 - Stephano

4

您考虑过使用此处建议的克隆函数吗?

function clone(obj){
    if(obj == null || typeof(obj) != 'object'){
        return obj;
    }

    var temp = new obj.constructor();
    for(var key in obj){
        temp[key] = clone(obj[key]);
    }
    return temp;
}

var MyClass = function(param1, param2) {};
var myObj = new MyClass(1,2);
var myObjClone = clone(myObj);
alert(myObj instanceof MyClass);      // => true
alert(myObjClone instanceof MyClass); // => true

这是一个不错的开始,但如果构造函数需要参数,则克隆会失败: var MyClass = function(param1, param2) {alert(param1.test)}; - Tom
汤姆,你认为这个“clone”函数会从哪里获取预期的参数?它怎么知道呢? - James
@J-P - 这就是我的问题所在。有没有一种方法可以获得一个精确的克隆,保留类型信息,而不需要知道要克隆的对象的任何信息。我现在假设这是不可能的。 - Tom

2

在StackOverflow上参考了一些答案后,我设计了一个非常灵活的函数。即使对象或其任何子对象具有需要参数的构造函数(感谢Object.create),该函数仍然有效。

(感谢Justin McCandless,现在它也支持循环引用。)

//If Object.create isn't already defined, we just do the simple shim, without the second argument,
//since that's all we need here
var object_create = Object.create;
if (typeof object_create !== 'function') {
    object_create = function(o) {
        function F() {}
        F.prototype = o;
        return new F();
    };
}

/**
 * Deep copy an object (make copies of all its object properties, sub-properties, etc.)
 * An improved version of http://keithdevens.com/weblog/archive/2007/Jun/07/javascript.clone
 * that doesn't break if the constructor has required parameters
 * 
 * It also borrows some code from https://dev59.com/A3RB5IYBdhLWcg3wET5J#11621004
 */ 
function deepCopy(src, /* INTERNAL */ _visited, _copiesVisited) {
    if(src === null || typeof(src) !== 'object'){
        return src;
    }

    //Honor native/custom clone methods
    if(typeof src.clone == 'function'){
        return src.clone(true);
    }

    //Special cases:
    //Date
    if(src instanceof Date){
        return new Date(src.getTime());
    }
    //RegExp
    if(src instanceof RegExp){
        return new RegExp(src);
    }
    //DOM Element
    if(src.nodeType && typeof src.cloneNode == 'function'){
        return src.cloneNode(true);
    }

    // Initialize the visited objects arrays if needed.
    // This is used to detect cyclic references.
    if (_visited === undefined){
        _visited = [];
        _copiesVisited = [];
    }

    // Check if this object has already been visited
    var i, len = _visited.length;
    for (i = 0; i < len; i++) {
        // If so, get the copy we already made
        if (src === _visited[i]) {
            return _copiesVisited[i];
        }
    }

    //Array
    if (Object.prototype.toString.call(src) == '[object Array]') {
        //[].slice() by itself would soft clone
        var ret = src.slice();

        //add it to the visited array
        _visited.push(src);
        _copiesVisited.push(ret);

        var i = ret.length;
        while (i--) {
            ret[i] = deepCopy(ret[i], _visited, _copiesVisited);
        }
        return ret;
    }

    //If we've reached here, we have a regular object

    //make sure the returned object has the same prototype as the original
    var proto = (Object.getPrototypeOf ? Object.getPrototypeOf(src): src.__proto__);
    if (!proto) {
        proto = src.constructor.prototype; //this line would probably only be reached by very old browsers 
    }
    var dest = object_create(proto);

    //add this object to the visited array
    _visited.push(src);
    _copiesVisited.push(dest);

    for (var key in src) {
        //Note: this does NOT preserve ES5 property attributes like 'writable', 'enumerable', etc.
        //For an example of how this could be modified to do so, see the singleMixin() function
        dest[key] = deepCopy(src[key], _visited, _copiesVisited);
    }
    return dest;
}

这个函数是我 simpleOO 库的一部分;任何错误修复或增强都将在那里进行(如果您发现了某些错误,请随时在 github 上开启一个问题)。

1
function clone( obj ) {  
    var target = new obj.constructor();  
    for ( var key in target ) { delete target[key]; }  
    return $.extend( true, target, obj );  
}  

$.extend不能复制所有不可见的内部属性(有些在Firefox中可见),但如果obj.constructor是正确的,并且没有参数错误,那么可以使用new obj.constructor()设置内部属性。如果你用类似于Derived.prototype = new Base()这样的东西来实现继承,你还需要跟着做Derived.prototype.constructor = Derived才能得到正确的构造函数。

你可以使用$.extend(true, new obj.constructor(), obj),但是可能构造函数创建了后来删除的属性--即使你可以正确获取构造函数参数--这就是为什么在执行扩展之前必须删除属性。构造函数参数错误并不重要,因为原始构造函数参数的影响--以及自那时以来发生在对象上的一切--都存在于我们正在克隆的对象中。


1
问题在于你传递了一个新对象到'{}'进行复制。这就是为什么你失去了类型。我发现,如果在传递真实对象之前将其包装起来,并在复制后取消包装复制的对象,extend将按预期保留类型。
function clone(obj)
{
    var wrappedObj = { inner: obj };
    var newObject = jQuery.extend(true, {}, wrappedObj);
    newObject = newObject.inner;
    return newObject;
}

我认为这是最好的答案,谢谢你,你太棒了 MAN :) - Fareed Alnamrouti

0

这里有一个替代方案,它并不完全复制或克隆任何东西,但应该可以得到所需的结果。

var myObj = new MyClass({a: 1},{a: 2});
var myObjCreator = MyClass.bind(this, {a: 1},{a: 2});
var myObjClone = new myObjCreator();

这里使用了Javascript的bind函数来创建一个对象,该对象会自动将给定的参数传递给MyClass构造函数。

我有类似于OP的要求,这个方法对我很有效,所以我想分享一下,但我也意识到有些人可能需要在修改后的对象上进行真正的深度复制,而这种方法并不适用。


0
在Firefox中,您可以编写:
Object.prototype.clone = function() {
  return eval(uneval(this));
}

可以像这样使用:

object1 = object2.clone();

答案在这里找到:source

但这只是Firefox的魔法。其他浏览器可能会在这里崩溃。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接