Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Javascript Deep Clone Object with Circular References

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;
like image 407
Lolums Avatar asked Oct 27 '16 18:10

Lolums


People also ask

What is the most efficient way to deep clone an object in JavaScript?

According to the benchmark test, the fastest way to deep clone an object in javascript is to use lodash deep clone function since Object. assign supports only shallow copy.

How do you deep copy an object?

Copy an Object With Object.assign() was the most popular way to deep copy an object. Object. assign() will copy everything into the new object, including any functions. Mutating the copied object also doesn't affect the original object.


4 Answers

There is now structuredClone in the Web API which also works with circular references.


Previous answer

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
like image 165
trincot Avatar answered Oct 19 '22 02:10

trincot


Since object cloning has a lot of pitfalls (circular references, proto chains, Set/Map, etc)
I suggest you to use one of well-tested popular solutions.

Like, lodash's _.cloneDeep or 'clone' npm module.

like image 10
artin Avatar answered Oct 19 '22 02:10

artin


You can store the references and results in separate arrays and when you find a property with the same reference you just need to return the cached result.

function deepClone(o) {
    var references = [];
    var cachedResults = [];

    function clone(obj) {
        if (typeof obj !== 'object')
            return obj;
        var index = references.indexOf(obj);
        if (index !== -1)
            return cachedResults[index];
        references.push(obj);
        var result = Array.isArray(obj) ? [] :
            obj.constructor ? new obj.constructor() : {};
        cachedResults.push(result);
        for (var key in obj)
            if (obj.hasOwnProperty(key))
                result[key] = clone(obj[key]);
        return result;
    }
    return clone(o);
}

I removed the map and some of the other type comparisons to make it more readable.

Check @trincot's solid ES6 answer if you can target modern browsers.

like image 1
A1rPun Avatar answered Oct 19 '22 00:10

A1rPun


const cloneDeep = src => {
  const clones = new WeakMap()
  return (function baseClone(src) {
    if (src !== Object(src)) {
      return src
    }
    if (clones.has(src)) {
      return clones.get(src)
    }
    const clone = {}
    clones.set(src, clone)
    Object.entries(src).forEach(([k, v]) => (clone[k] = baseClone(v)))
    return clone
  })(src)
}

const a = { x: 1 }
a.y = a
console.log(cloneDeep(a)) // <ref *1> { x: 1, y: [Circular *1] }
like image 1
Wenfang Du Avatar answered Oct 19 '22 00:10

Wenfang Du