How to Deep clone in javascript

2018-12-31 14:42发布

问题:

How do you deep clone a Javascript object?

I know there are various functions based on frameworks like JSON.parse(JSON.stringify(o)) and $.extend(true, {}, o) but I don\'t want to use a framework like that.

What is the most elegant or efficient way to create a deep clone.

We do care about edge cases like cloning array\'s. Not breaking prototype chains, dealing with self reference.

We don\'t care about supporting copying of DOM objects and like because .cloneNode exists for that reason.

As I mainly want to use deep clones in node.js using ES5 features of the V8 engine is acceptable.

[Edit]

Before anyone suggests let me mention there is a distinct difference between creating a copy by prototypically inheriting from the object and cloning it. The former makes a mess of the prototype chain.

[Further Edit]

After reading your answer I came to the annoying discovery that cloning entire objects is a very dangerous and difficult game. Take for example the following closure based object

var o = (function() {
     var magic = 42;

     var magicContainer = function() {
          this.get = function() { return magic; };
          this.set = function(i) { magic = i; };
     }

      return new magicContainer;
}());

var n = clone(o); // how to implement clone to support closures

Is there any way to write a clone function that clones the object, has the same state at time of cloning but cannot alter the state of o without writing a JS parser in JS.

There should be no real world need for such a function anymore. This is mere academic interest.

回答1:

It really depends what you would like to clone. Is this a truly JSON object or just any object in JavaScript? If you would like to do any clone, it might get you into some trouble. Which trouble? I will explain it below, but first, a code example which clones object literals, any primitives, arrays and DOM nodes.

function clone(item) {
    if (!item) { return item; } // null, undefined values check

    var types = [ Number, String, Boolean ], 
        result;

    // normalizing primitives if someone did new String(\'aaa\'), or new Number(\'444\');
    types.forEach(function(type) {
        if (item instanceof type) {
            result = type( item );
        }
    });

    if (typeof result == \"undefined\") {
        if (Object.prototype.toString.call( item ) === \"[object Array]\") {
            result = [];
            item.forEach(function(child, index, array) { 
                result[index] = clone( child );
            });
        } else if (typeof item == \"object\") {
            // testing that this is DOM
            if (item.nodeType && typeof item.cloneNode == \"function\") {
                result = item.cloneNode( true );    
            } else if (!item.prototype) { // check that this is a literal
                if (item instanceof Date) {
                    result = new Date(item);
                } else {
                    // it is an object literal
                    result = {};
                    for (var i in item) {
                        result[i] = clone( item[i] );
                    }
                }
            } else {
                // depending what you would like here,
                // just keep the reference, or create new object
                if (false && item.constructor) {
                    // would not advice to do that, reason? Read below
                    result = new item.constructor();
                } else {
                    result = item;
                }
            }
        } else {
            result = item;
        }
    }

    return result;
}

var copy = clone({
    one : {
        \'one-one\' : new String(\"hello\"),
        \'one-two\' : [
            \"one\", \"two\", true, \"four\"
        ]
    },
    two : document.createElement(\"div\"),
    three : [
        {
            name : \"three-one\",
            number : new Number(\"100\"),
            obj : new function() {
                this.name = \"Object test\";
            }   
        }
    ]
})

And now, let\'s talk about problems you might get when start cloning REAL objects. I\'m talking now, about objects which you create by doing something like

var User = function(){}
var newuser = new User();

Of course you can clone them, it\'s not a problem, every object expose constructor property, and you can use it to clone objects, but it will not always work. You also can do simple for in on this objects, but it goes to the same direction - trouble. I have also included clone functionality inside the code, but it\'s excluded by if( false ) statement.

So, why cloning can be a pain? Well, first of all, every object/instance might have some state. You never can be sure that your objects doesn\'t have for example an private variables, and if this is the case, by cloning object, you just break the state.

Imagine there is no state, that\'s fine. Then we still have another problem. Cloning via \"constructor\" method will give us another obstacle. It\'s an arguments dependency. You never can be sure, that someone who created this object, did not did, some kind of

new User({
   bike : someBikeInstance
});

If this is the case, you are out of luck, someBikeInstance was probably created in some context and that context is unkown for clone method.

So what to do? You still can do for in solution, and treat such objects like normal object literals, but maybe it\'s an idea not to clone such objects at all, and just pass the reference of this object?

Another solution is - you could set a convention that all objects which must be cloned should implement this part by themselves and provide appropriate API method ( like cloneObject ). Something what cloneNode is doing for DOM.

You decide.



回答2:

Very simple way, maybe too simple:

var cloned = JSON.parse(JSON.stringify(objectToClone));


回答3:

The JSON.parse(JSON.stringify()) combination to deep copy Javascript objects is an ineffective hack, because JSON does not support values of undefined and function () {}, and therefore JSON.stringify will ignore those sections of code, when \"stringifying\" (marshalling) the Javascript object into JSON.

The following function will deep copy objects, and does not require a 3rd party library (jQuery, LoDash, etc).

function copy(aObject) {
  if (!aObject) {
    return aObject;
  }

  let v;
  let bObject = Array.isArray(aObject) ? [] : {};
  for (const k in aObject) {
    v = aObject[k];
    bObject[k] = (typeof v === \"object\") ? copy(v) : v;
  }

  return bObject;
}


回答4:

Here is an ES6 function that will also work for objects with cyclic references:

function deepClone(obj, hash = new WeakMap()) {
    if (Object(obj) !== obj) return obj; // primitives
    if (obj instanceof Set) return new Set(obj); // See note about this!
    if (hash.has(obj)) return hash.get(obj); // cyclic reference
    const result = obj instanceof Date ? new Date(obj)
                 : obj instanceof RegExp ? new RegExp(obj.source, obj.flags)
                 : obj.constructor ? new obj.constructor() 
                 : Object.create(null);
    hash.set(obj, result);
    if (obj instanceof Map)
        Array.from(obj, ([key, val]) => result.set(key, deepClone(val, hash)) );
    return Object.assign(result, ...Object.keys(obj).map (
        key => ({ [key]: deepClone(obj[key], hash) }) ));
}

// Sample data
var p = {
  data: 1,
  children: [{
    data: 2,
    parent: null
  }]
};
p.children[0].parent = p;

var q = deepClone(p);

console.log(q.children[0].parent.data); // 1

A note about Sets and Maps

How to deal with the keys of Sets and Maps is debatable: those keys are often primitives (in which case there is no debate), but they can also be objects. In that case the question becomes: should those keys be cloned?

One could argue that this should be done, so that if those objects are mutated in the copy, the objects in the original are not affected, and vice versa.

On the other hand one would want that if a Set/Map has a key, this should be true in both the original and the copy -- at least before any change is made to either of them. It would be strange if the copy would be a Set/Map that has keys that never occurred before (as they were created during the cloning process): surely that is not very useful for any code that needs to know whether a given object is a key in that Set/Map or not.

As you notice, I am more of the second opinion: the keys of Sets and Maps are values (maybe references) that should remain the same.

Such choices will often also surface with other (maybe custom) objects. There is no general solution, as much depends on how the cloned object is expected to behave in your specific case.



回答5:

The Underscore.js contrib library library has a function called snapshot that deep clones an object

snippet from the source:

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

  var temp = new obj.constructor();

  for(var key in obj) {
    if (obj.hasOwnProperty(key)) {
      temp[key] = _.snapshot(obj[key]);
    }
  }

  return temp;
}

once the library is linked to your project, invoke the function simply using

_.snapshot(object);


回答6:

This is the deep cloning method I use, I think it Great, hope you make suggestions

function deepClone (obj) {
    var _out = new obj.constructor;

    var getType = function (n) {
        return Object.prototype.toString.call(n).slice(8, -1);
    }

    for (var _key in obj) {
        if (obj.hasOwnProperty(_key)) {
            _out[_key] = getType(obj[_key]) === \'Object\' || getType(obj[_key]) === \'Array\' ? deepClone(obj[_key]) : obj[_key];
        }
    }
    return _out;
}


回答7:

Lo-Dash, now a superset of Underscore.js, has a couple of deep clone functions:

  • _.cloneDeep(object)
  • _.cloneDeepWith(object, (val) => {if(_.isElement(val)) return val.cloneNode(true)})

    the second parameter is a function that is invoked to produce the cloned value.

From an answer of the author himself:

lodash underscore build is provided to ensure compatibility with the latest stable version of Underscore.



回答8:

As others have noted on this and similar questions, cloning an \"object\", in the general sense, is dubious in JavaScript.

However, there is a class of objects, which I call \"data\" objects, that is, those constructed simply from { ... } literals and/or simple property assignments or deserialized from JSON for which it is reasonable to want to clone. Just today I wanted to artificially inflate data received from a server by 5x to test what happens for a large data set, but the object (an array) and its children had to be distinct objects for things to function correctly. Cloning allowed me to do this to multiply my data set:

return dta.concat(clone(dta),clone(dta),clone(dta),clone(dta));

The other place I often end up cloning data objects is for submitting data back to the host where I want to strip state fields from the object in the data model before sending it. For example, I might want to strip all fields starting with \"_\" from the object as it is cloned.

This is the code I ended up writing to do this generically, including supporting arrays and a selector to choose which members to clone (which uses a \"path\" string to determine context):

function clone(obj,sel) {
    return (obj ? _clone(\"\",obj,sel) : obj);
    }

function _clone(pth,src,sel) {
    var ret=(src instanceof Array ? [] : {});

    for(var key in src) {
        if(!src.hasOwnProperty(key)) { continue; }

        var val=src[key], sub;

        if(sel) {
            sub+=pth+\"/\"+key;
            if(!sel(sub,key,val)) { continue; }
            }

        if(val && typeof(val)==\'object\') {
            if     (val instanceof Boolean) { val=Boolean(val);        }
            else if(val instanceof Number ) { val=Number (val);        }
            else if(val instanceof String ) { val=String (val);        }
            else                            { val=_clone(sub,val,sel); }
            }
        ret[key]=val;
        }
    return ret;
    }

The simplest reasonable deep-clone solution, assuming a non-null root object and with no member selection is:

function clone(src) {
    var ret=(src instanceof Array ? [] : {});
    for(var key in src) {
        if(!src.hasOwnProperty(key)) { continue; }
        var val=src[key];
        if(val && typeof(val)==\'object\') { val=clone(val);  }
        ret[key]=val;
        }
    return ret;
    }


回答9:

I noticed that Map should require special treatment, thus with all suggestions in this thread, code will be:

function deepClone( obj ) {
    if( !obj || true == obj ) //this also handles boolean as true and false
        return obj;
    var objType = typeof( obj );
    if( \"number\" == objType || \"string\" == objType ) // add your immutables here
        return obj;
    var result = Array.isArray( obj ) ? [] : !obj.constructor ? {} : new obj.constructor();
    if( obj instanceof Map )
        for( var key of obj.keys() )
            result.set( key, deepClone( obj.get( key ) ) );
    for( var key in obj )
        if( obj.hasOwnProperty( key ) )
            result[key] = deepClone( obj[ key ] );
    return result;
}


回答10:

This works for arrays, objects and primitives. Doubly recursive algorithm that switches between two traversal methods:

const deepClone = (objOrArray) => {

  const copyArray = (arr) => {
    let arrayResult = [];
    arr.forEach(el => {
        arrayResult.push(cloneObjOrArray(el));
    });
    return arrayResult;
  }

  const copyObj = (obj) => {
    let objResult = {};
    for (key in obj) {
      if (obj.hasOwnProperty(key)) {
        objResult[key] = cloneObjOrArray(obj[key]);
      }
    }
    return objResult;
  }

  const cloneObjOrArray = (el) => {
    if (Array.isArray(el)) {
      return copyArray(el);
    } else if (typeof el === \'object\') {
      return copyObj(el);
    } else {
      return el;
    }
  }

  return cloneObjOrArray(objOrArray);
}


回答11:

We can utilize recursion for making deepCopy. It can create copy of array, object, array of object, object with function. if you want, you can add function for other type of data structure like map etc.

function deepClone(obj) {
         var retObj;
        _assignProps = function(obj, keyIndex, retObj) {
               var subType = Object.prototype.toString.call(obj[keyIndex]);
               if(subType === \"[object Object]\" || subType === \"[object Array]\") {
                    retObj[keyIndex] = deepClone(obj[keyIndex]);
               }
               else {
                     retObj[keyIndex] = obj[keyIndex];
               }
        };

        if(Object.prototype.toString.call(obj) === \"[object Object]\") {
           retObj = {};
           for(key in obj) {
               this._assignProps(obj, key, retObj);
           }
        }
        else if(Object.prototype.toString.call(obj) == \"[object Array]\") {
           retObj = [];
           for(var i = 0; i< obj.length; i++) {
              this._assignProps(obj, i, retObj);
            }
        };

        return retObj;
    };


回答12:

There should be no real world need for such a function anymore. This is mere academic interest.

As purely an exercise, this is a more functional way of doing it. It\'s an extension of @tfmontague\'s answer as I\'d suggested adding a guard block there. But seeing as I feel compelled to ES6 and functionalise all the things, here\'s my pimped version. It complicates the logic as you have to map over the array and reduce over the object, but it avoids any mutations.

function cloner(x) {
    const recurseObj = x => typeof x === \'object\' ? cloner(x) : x
    const cloneObj = (y, k) => {
        y[k] = recurseObj(x[k])
        return y
    }
    // Guard blocks
    // Add extra for Date / RegExp if you want
    if (!x) {
        return x
    }
    if (Array.isArray(x)) {
        return x.map(recurseObj)
    }
    return Object.keys(x).reduce(cloneObj, {})
}
const tests = [
    null,
    [],
    {},
    [1,2,3],
    [1,2,3, null],
    [1,2,3, null, {}],
    [new Date(\'2001-01-01\')], // FAIL doesn\'t work with Date
    {x:\'\', y: {yx: \'zz\', yy: null}, z: [1,2,3,null]},
    {
        obj : new function() {
            this.name = \"Object test\";
        }
    } // FAIL doesn\'t handle functions
]
tests.map((x,i) => console.log(i, cloner(x)))



回答13:

my addition to all the answers

deepCopy = arr => {
  if (typeof arr !== \'object\') return arr
  if(arr.pop) return [...arr].map(deepCopy)
  const copy = {}
  for (let prop in arr)
    copy[prop] = deepCopy(arr[prop])
  return copy
}


标签: javascript