Javascript Deep Clone Object with Circular References

Lolums picture Lolums · Oct 27, 2016 · Viewed 12.6k times · Source

I have copied the function below from an existing answer by Dmitriy Pichugin. This function can deep clone an object without any circular references- it works.

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

However, my program loops indefinitely and I have realised that this is due to a circular reference.

An example of a circular reference:

function A() {}
function B() {}

var a = new A();
var b = new B();

a.b = b;
b.a = a;

Answer

trincot picture trincot · Oct 27, 2016

I would suggest to use a Map to map objects in the source with their copy in the destination. In fact, I ended up using WeakMap as suggested by Bergi. Whenever a source object is in the map, its corresponding copy is returned instead of recursing further.

At the same time some of the code in the original deepClone code can be optimised further:

  • The first part testing for primitive values has a small issue: it treats new Number(1) differently from new Number(2). This is because of the == in the first if. It should be changed to ===. But really, the first few lines of code seem then equivalent to this test: Object(obj) !== obj

  • I also rewrote some for loops into more functional expressions

This needs ES6 support:

function deepClone(obj, hash = new WeakMap()) {
    // Do not try to clone primitives or functions
    if (Object(obj) !== obj || obj instanceof Function) return obj;
    if (hash.has(obj)) return hash.get(obj); // Cyclic reference
    try { // Try to run constructor (without arguments, as we don't know them)
        var result = new obj.constructor();
    } catch(e) { // Constructor failed, create object without running the constructor
        result = Object.create(Object.getPrototypeOf(obj));
    }
    // Optional: support for some standard constructors (extend as desired)
    if (obj instanceof Map)
        Array.from(obj, ([key, val]) => result.set(deepClone(key, hash), 
                                                   deepClone(val, hash)) );
    else if (obj instanceof Set)
        Array.from(obj, (key) => result.add(deepClone(key, hash)) );
    // Register in hash    
    hash.set(obj, result);
    // Clone and assign enumerable own properties recursively
    return Object.assign(result, ...Object.keys(obj).map (
        key => ({ [key]: deepClone(obj[key], hash) }) ));
}
// Sample data
function A() {}
function B() {}
var a = new A();
var b = new B();
a.b = b;
b.a = a;
// Test it
var c = deepClone(a);
console.log('a' in c.b.a.b); // true