Deep clone of JavaScript object

Summary

JavaScript object cloning frequently necessitates a deep copy to ensure complete independence from the original, especially for nested structures, contrasting with shallow clones that only duplicate references. Simple assignment loops or Object.assign() create shallow copies, meaning changes to nested objects in the clone will still affect the original. For a true deep clone, developers can leverage JSON.parse(JSON.stringify()), though this approach has drawbacks like dropping constructors, inability to clone functions or RegExp, and issues with circular references. More robust deep cloning often requires custom recursive functions that traverse the object graph, carefully handling different data types and preventing infinite loops. Libraries like jQuery also offer sophisticated deep cloning utilities, highlighting the complexity of a comprehensive solution.

In JavaScript world, it's frequently seen object clone. Normally when creating a clone of an object, it's only the reference being cloned but not all contents. In some scenarioes, it's desired to clone all the content instead of just the reference such that any change made to the cloned object will not change the original object.

Differences between shallow clone and deep clone can be as simple as:

  • Shallow clone : Only the object reference is cloned but not the content
  • Deep clone : Clone all content, including the content of the object referenced in the object

Shallow clone implementation

It's relative simple to implement shallow clone, only simple statements are needed.

Simple statement

function simpleClone(initalObj) {
    var obj = {};
    for ( var i in initalObj) {
        obj[i] = initalObj[i];
    }
    return obj;
}

Example calling code

var obj = {
    a: "hello",
    b: {
        a: "world",
        b: 21
    },
    c: ["Bob", "Tom", "Jenny"],
    d: function() {
        alert("hello world");
    }
}
var cloneObj = simpleClone(obj); // Object clone

console.log(cloneObj.b); // {a: "world", b: 21}
console.log(cloneObj.c); // ["Bob", "Tom", "Jenny"]
console.log(cloneObj.d); // function() { alert("hello world"); }

cloneObj.b.a = "changed";
cloneObj.c = [1, 2, 3];
cloneObj.d = function() { alert("changed"); };

console.log(obj.b); // {a: "changed", b: 21}

console.log(obj.c); // ["Bob", "Tom", "Jenny"]
console.log(obj.d); // function() { alert("hello world"); }

Object.assign()

Object.assign() can clone enumerable properties in original object to the target object and return the target object. But Object.assign() will only do shallow clone, the properties cloned are just object references but not object content referenced.

var obj = { a: {a: "hello", b: 21} };

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

initalObj.a.a = "changed";

console.log(obj.a.a); // "changed"

Deep clone implementation

The simplest method to implement deep clone is JSON.parse(), also recursive clone can be applied. ES5 also provides Object.create() for this use.

JSON.parse()

function deepClone(initalObj) {
    var obj = {};
    try {
        obj = JSON.parse(JSON.stringify(initalObj));
    }
    return obj;
}

Example calling code

var obj = {
    a: {
        a: "world",
        b: 21
    }
}
var cloneObj = deepClone(obj);
cloneObj.a.a = "changed";

console.log(obj.a.a); // "world"

This method is simple to use. But it also has some drawbacks. For example, it will drop the constructor of object, i.e, the constructor of the object will become Object no matter what object the original constructor is.

In addition, this method can only handle objects representable by JSON, these include Number, String, Boolean, Array, Plain object. RegExp cannot be cloned with this method.

Recursive clone

function deepClone(initalObj, finalObj) {
    var obj = finalObj || {};
    for (var i in initalObj) {
        if (typeof initalObj[i] === 'object') {
            obj[i] = (initalObj[i].constructor === Array) ? [] : {};
            arguments.callee(initalObj[i], obj[i]);
        } else {
            obj[i] = initalObj[i];
        }
    }
    return obj;
}

This method can achieve object deep clone, but it will fall into an infinite loop if two objects are mutual referenced.

An improved version

function deepClone(initalObj, finalObj) {
    var obj = finalObj || {};
    for (var i in initalObj) {
        var prop = initalObj[i];

        // Avoid infinite loop
        if(prop === obj) {
            continue;
        }

        if (typeof prop === 'object') {
            obj[i] = (prop.constructor === Array) ? [] : {};
            arguments.callee(prop, obj[i]);
        } else {
            obj[i] = prop;
        }
    }
    return obj;
}

Object.create()

function deepClone(initalObj, finalObj) {
    var obj = finalObj || {};
    for (var i in initalObj) {
        var prop = initalObj[i];

        if(prop === obj) {
            continue;
        }

        if (typeof prop === 'object') {
            if(prop.constructor === Array) {
                obj[i] = deepClone(prop, []);
            } else {
                obj[i] = Object.create(prop);
            }
        } else {
            obj[i] = prop;
        }
    }
    return obj;
}

jQuery.extend() implementation

jQuery.extend() also implements object deep clone. Below is the official code for reference.  

jQuery.extend = jQuery.fn.extend = function() {
    var options, name, src, copy, copyIsArray, clone,
        target = arguments[ 0 ] || {},
        i = 1,
        length = arguments.length,
        deep = false;

    // Handle a deep copy situation
    if ( typeof target === "boolean" ) {
        deep = target;

        // Skip the boolean and the target
        target = arguments[ i ] || {};
        i++;
    }

    // Handle case when target is a string or something (possible in deep copy)
    if ( typeof target !== "object" && !jQuery.isFunction( target ) ) {
        target = {};
    }

    // Extend jQuery itself if only one argument is passed
    if ( i === length ) {
        target = this;
        i--;
    }

    for ( ; i < length; i++ ) {

        // Only deal with non-null/undefined values
        if ( ( options = arguments[ i ] ) != null ) {

            // Extend the base object
            for ( name in options ) {
                src = target[ name ];
                copy = options[ name ];

                // Prevent never-ending loop
                if ( target === copy ) {
                    continue;
                }

                // Recurse if we're merging plain objects or arrays
                if ( deep && copy && ( jQuery.isPlainObject( copy ) ||
                    ( copyIsArray = jQuery.isArray( copy ) ) ) ) {

                    if ( copyIsArray ) {
                        copyIsArray = false;
                        clone = src && jQuery.isArray( src ) ? src : [];

                    } else {
                        clone = src && jQuery.isPlainObject( src ) ? src : {};
                    }

                    // Never move original objects, clone them
                    target[ name ] = jQuery.extend( deep, clone, copy );

                // Don't bring in undefined values
                } else if ( copy !== undefined ) {
                    target[ name ] = copy;
                }
            }
        }
    }

    // Return the modified object
    return target;
};

Reference : http://www.codeceo.com/article/javascript-object-deep-copy.html

JAAVSCRIPT SHALLOW COPY DEEP CLONE DEEP COPY SHALLOW CLONE

  RELATED

  COMMENTS

0

No comment for this article.