javascript - 对象复制 - 深克隆和浅克隆js




如何正确克隆JavaScript对象? (20)

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

Object.assign方法是ECMAScript 2015(ES6)标准的一部分,可以完全满足您的需求。

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

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

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

如何正确克隆JavaScript对象?


A.Levy的答案几乎完成,这是我的小贡献: 有一种方法如何处理递归引用 ,请参阅此行

if(this[attr]==this) copy[attr] = copy;

如果对象是XML DOM元素,我们必须使用cloneNode

if(this.cloneNode) return this.cloneNode(true);

受A.Levy详尽的研究和Calvin的原型设计方法的启发,我提供了这个解决方案:

Object.prototype.clone = function() {
  if(this.cloneNode) return this.cloneNode(true);
  var copy = this instanceof Array ? [] : {};
  for(var attr in this) {
    if(typeof this[attr] == "function" || this[attr]==null || !this[attr].clone)
      copy[attr] = this[attr];
    else if(this[attr]==this) copy[attr] = copy;
    else copy[attr] = this[attr].clone();
  }
  return copy;
}

Date.prototype.clone = function() {
  var copy = new Date();
  copy.setTime(this.getTime());
  return copy;
}

Number.prototype.clone = 
Boolean.prototype.clone =
String.prototype.clone = function() {
  return this;
}

另见Andy Burke在答案中的注释。


一个特别不优雅的解决方案是使用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

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

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

寻求优雅解决方案的另一个障碍是正确设置原型继承的问题。 如果你的源对象的原型是Object ,那么简单地用{}创建一个新的通用对象就行了,但是如果源的原型是Object一些后代,那么你将会缺少你使用它跳过的原型中的其他成员。 hasOwnProperty过滤器,或原型中的过滤器,但首先不能枚举。 一种解决方案可能是调用源对象的constructor属性来获取初始复制对象,然后复制属性,但是您仍然不会获得不可枚举的属性。 例如, 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与另一个相同的方法是调用setTime方法,但这特定于Date类。 我不认为这个问题有一个防弹的一般解决方案,但我会很高兴出错!

当我不得不实现一般的深度复制时,我最终通过假设我只需要复制一个普通的ObjectArrayDateStringNumberBoolean来妥协。 最后3种类型是不可变的,所以我可以执行浅拷贝而不用担心它会改变。 我进一步假设ObjectArray包含的任何元素也将是该列表中的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  due to infinite recursion:
var cyclicGraph = {
    "left"  : { "left" : null, "right" : null, "data" : 3 },
    "data"  : 8
};
cyclicGraph["right"] = cyclicGraph;

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


从这篇文章: 如何在Javascript中复制数组和对象由Brian Huisman:

Object.prototype.clone = function() {
  var newObj = (this instanceof Array) ? [] : {};
  for (var i in this) {
    if (i == 'clone') continue;
    if (this[i] && typeof this[i] == "object") {
      newObj[i] = this[i].clone();
    } else newObj[i] = this[i]
  } return newObj;
};

使用Lodash:

var y = _.clone(x, true);

在ECMAScript 2018中

let objClone = { ...obj };

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


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

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

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


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

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

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

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


对于那些使用AngularJS的人来说,还有直接的方法来克隆或扩展这个库中的对象。

var destination = angular.copy(source);

要么

angular.copy(source, destination);

更多angular.copy documentation ...


您可以简单地使用spread属性来复制没有引用的对象。 但要小心(请参阅注释),'copy'只是在最低的对象/数组级别。 嵌套属性仍然是引用!

完整克隆:

let x = {a: 'value1'}
let x2 = {...x}

// => mutate without references:

x2.a = 'value2'
console.log(x.a)    // => 'value1'

克隆二级引用:

const y = {a: {b: 'value3'}}
const y2 = {...y}

// => nested object is still a references:

y2.a.b = 'value4'
console.log(y.a.b)    // => 'value4'

JavaScript本身实际上不支持深度克隆。 使用实用程序功能。 例如Ramda:

http://ramdajs.com/docs/#clone


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

因此,这不是问题的确切答案,但它是一个单行解决方案,因而优雅。 它适用于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。


这是您可以使用的功能。

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

JanTotoň的答案非常接近,由于兼容性问题,可能是最好在浏览器中使用,但它可能会导致一些奇怪的枚举问题。例如,执行:

for ( var i in someArray ) { ... }

迭代遍历数组的元素后,将clone()方法赋给i。这是一个避免枚举并适用于node.js的改编:

Object.defineProperty( Object.prototype, "clone", {
    value: function() {
        if ( this.cloneNode )
        {
            return this.cloneNode( true );
        }

        var copy = this instanceof Array ? [] : {};
        for( var attr in this )
        {
            if ( typeof this[ attr ] == "function" || this[ attr ] == null || !this[ attr ].clone )
            {
                copy[ attr ] = this[ attr ];
            }
            else if ( this[ attr ] == this )
            {
                copy[ attr ] = copy;
            }
            else
            {
                copy[ attr ] = this[ attr ].clone();
            }
        }
        return copy;
    }
});

Object.defineProperty( Date.prototype, "clone", {
    value: function() {
        var copy = new Date();
        copy.setTime( this.getTime() );
        return copy;
    }
});

Object.defineProperty( Number.prototype, "clone", { value: function() { return this; } } );
Object.defineProperty( Boolean.prototype, "clone", { value: function() { return this; } } );
Object.defineProperty( String.prototype, "clone", { value: function() { return this; } } );

这避免了使clone()方法可枚举,因为defineProperty()默认为可枚举为false。


对旧问题的新答案!如果您有幸使用带有Spread语法的 ECMAScript 2016(ES6),那很容易。

keepMeTheSame = {first: "Me!", second: "You!"};
cloned = {...keepMeTheSame}

这为对象的浅拷贝提供了一种干净的方法。制作深层复制,意味着在每个递归嵌套对象中设置每个值的新副本,需要上面较重的解决方案。

JavaScript不断发展。


您可以使用一行代码克隆对象并从前一个引用中删除任何引用。简单地说:

var obj1 = { text: 'moo1' };
var obj2 = Object.create(obj1); // Creates a new clone without references

obj2.text = 'moo2'; // Only updates obj2's text property

console.log(obj1, obj2); // Outputs: obj1: {text:'moo1'}, obj2: {text:'moo2'}

对于当前不支持Object.create的浏览器/引擎,您可以使用此polyfill:

// Polyfill Object.create if it does not exist
if (!Object.create) {
    Object.create = function (o) {
        var F = function () {};
        F.prototype = o;
        return new F();
    };
}

我只想添加到Object.create这篇文章中的所有解决方案,这对于nodejs来说并不是以所需的方式工作。

在Firefox中的结果

var a = {"test":"test"};
var b = Object.create(a);
console.log(b);´

{test:"test"}

在nodejs中

{}

由于mindeavor声明要克隆的对象是“文字构造的”对象,因此解决方案可能是简单地多次生成对象而不是克隆对象的实例:

function createMyObject()
{
    var myObject =
    {
        ...
    };
    return myObject;
}

var myObjectInstance1 = createMyObject();
var myObjectInstance2 = createMyObject();

这是A. Levy的代码的改编,也用于处理函数和多个/循环引用的克隆 - 这意味着如果克隆的树中的两个属性是同一对象的引用,则克隆的对象树将具有这些属性指向引用对象的同一个克隆。这也解决了循环依赖的情况,如果不处理,会导致无限循环。算法的复杂性是O(n)

function clone(obj){
    var clonedObjectsArray = [];
    var originalObjectsArray = []; //used to remove the unique ids when finished
    var next_objid = 0;

    function objectId(obj) {
        if (obj == null) return null;
        if (obj.__obj_id == undefined){
            obj.__obj_id = next_objid++;
            originalObjectsArray[obj.__obj_id] = obj;
        }
        return obj.__obj_id;
    }

    function cloneRecursive(obj) {
        if (null == obj || typeof obj == "string" || typeof obj == "number" || typeof obj == "boolean") return obj;

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

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

        // Handle Object
        if (obj instanceof Object) {
            if (clonedObjectsArray[objectId(obj)] != undefined)
                return clonedObjectsArray[objectId(obj)];

            var copy;
            if (obj instanceof Function)//Handle Function
                copy = function(){return obj.apply(this, arguments);};
            else
                copy = {};

            clonedObjectsArray[objectId(obj)] = copy;

            for (var attr in obj)
                if (attr != "__obj_id" && obj.hasOwnProperty(attr))
                    copy[attr] = cloneRecursive(obj[attr]);                 

            return copy;
        }       


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



    //remove the unique ids
    for (var i = 0; i < originalObjectsArray.length; i++)
    {
        delete originalObjectsArray[i].__obj_id;
    };

    return cloneObj;
}

一些快速测试

var auxobj = {
    prop1 : "prop1 aux val", 
    prop2 : ["prop2 item1", "prop2 item2"]
    };

var obj = new Object();
obj.prop1 = "prop1_value";
obj.prop2 = [auxobj, auxobj, "some extra val", undefined];
obj.nr = 3465;
obj.bool = true;

obj.f1 = function (){
    this.prop1 = "prop1 val changed by f1";
};

objclone = clone(obj);

//some tests i've made
console.log("test number, boolean and string cloning: " + (objclone.prop1 == obj.prop1 && objclone.nr == obj.nr && objclone.bool == obj.bool));

objclone.f1();
console.log("test function cloning 1: " + (objclone.prop1 == 'prop1 val changed by f1'));
objclone.f1.prop = 'some prop';
console.log("test function cloning 2: " + (obj.f1.prop == undefined));

objclone.prop2[0].prop1 = "prop1 aux val NEW";
console.log("test multiple references cloning 1: " + (objclone.prop2[1].prop1 == objclone.prop2[0].prop1));
console.log("test multiple references cloning 2: " + (objclone.prop2[1].prop1 != obj.prop2[0].prop1));

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




clone