22

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;
Lucien
  • 776
  • 3
  • 12
  • 40
  • You could create an array which you store the object references for every "to-clone" object and check with `.indexOf()` if the reference is stored in that list. – A1rPun Oct 27 '16 at 18:48
  • 1
    You might want to look at [JSON.stringify, avoid TypeError: Converting circular structure to JSON](http://stackoverflow.com/q/11616630/215552), and [JavaScript: Deep Copy Circular JSON](http://stackoverflow.com/q/30064997/215552) – Heretic Monkey Oct 27 '16 at 18:53
  • @MikeMcCaughan ah, interesting! Ty! It looks pretty useful but does not preserve custom class instances. – Lucien Oct 27 '16 at 19:52

5 Answers5

19

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
trincot
  • 317,000
  • 35
  • 244
  • 286
  • I believe this will fail if one of my object constructors will expect to receive arguments. `function A(foo) {if (!foo) throw Error('pass foo!')}` – artin Apr 01 '17 at 14:01
  • 1
    @Artin, how would the *deepClone* function know which argument(s) to pass to the constructor? As far as I know, JavaScript objects to not maintain information about which arguments were passed to the constructor. – trincot Apr 01 '17 at 14:40
  • @Artin, the alternative of not calling the constructor (as you suggested in a deleted comment) with `Object.create(obj.constructor)` might lead to other issues: running the constructor might be essential for the object to be in a consistent state and it will not have any own properties. – trincot Apr 01 '17 at 20:17
  • @trincon, can you provide an example? as i see it works ok http://jsbin.com/bijaloziko/1/edit?js,console – artin Apr 01 '17 at 20:35
  • What if you define a method in the constructor that returns `foo`? – trincot Apr 01 '17 at 20:42
  • Another example: if `a` is a `Set`, then the copy object will have a malfunctioning `values` method. – trincot Apr 01 '17 at 20:47
  • @Artin, I adapted the code to use your idea, but as a fall-back solution. It is indeed better to go for that (if running the constructor fails) than failing with an exception. Thanks! – trincot Apr 01 '17 at 20:54
  • well if you define a method inside constructor it should be copied as any other primitive instance members. only reason it does not work in current solution is because of `Object(obj) !== obj` check. So if you add separate `if` check for `function` it just copied with primitives. http://jsbin.com/fokogunezu/1/edit?js,console – artin Apr 01 '17 at 21:09
  • The method was a bad example: functions get created in my deepClone function, but they have no functionality. Just referencing the original function is a solution, although it goes against the goal of cloning. Imagine a function that references a private variable within the constructor... so via such a method, the clone object will share part of the state of the original object. There are no perfect solutions for this. – trincot Apr 01 '17 at 21:23
10

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.

artin
  • 1,764
  • 1
  • 17
  • 23
  • 5
    That's the only reasonable answer to this question, there is no point in reinventing the wheel. – Kamil Bęben Jun 21 '19 at 11:47
  • @KamilBęben If it's trivial to implement a solution yourself, you should do it 10 out of 10 times. When you use a library it brings with it a bunch of considerations you never made and is opinionated in a way you might not be. – Kevin Beal Jan 06 '21 at 20:44
1

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.

A1rPun
  • 16,287
  • 7
  • 57
  • 90
  • Ah, I get the exception `Uncaught RangeError: Maximum call stack size exceeded(…)`, which appears to be due to repeated calls on line 19 of the `deepClone` function. I used it on the example `a` and `b` objects in my question. – Lucien Oct 27 '16 at 19:49
  • 1
    @Lolums I updated my answer with the corrects checks. Notice that the function is a little bit stripped down to make it more understandable :) – A1rPun Oct 27 '16 at 20:25
  • 2
    Instead of using `references` and `cachedResults`, this is a perfect use case for a `WeakMap`! – Bergi Oct 27 '16 at 20:29
  • @Bergi That would be a great addition! – A1rPun Oct 27 '16 at 20:39
  • Thank you- it works now. Well, almost: `deepClone(a)` returns `A { b: B {a: undefined} }` – Lucien Oct 27 '16 at 20:41
1
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] }
Wenfang Du
  • 8,804
  • 9
  • 59
  • 90
1

The basic idea is that you want to avoid a "Maximum callstack exceeded" situation.

In order to do that, when it comes time to clone an object, you need to first create an empty object, cache it (with something like a WeakMap) and then update the properties on that cached object. If you try to cache it after creating the copy, it will not be cached by the time it will be recursively referenced again (and thus the call stack error).

You want to do something like:

function deepClone(value, cache = new WeakMap()) {
  if(cache.has(value)) {
    return cache.get(value)
  }
  if(isPrimitive(value) || isFunction(value)) {
    return value;
  }
  if(value instanceof AnyParticularClass) {
    // handle specially in any cases you plan to support
  }
  if(isObject(value)) {
    const result = {}
    cache.set(value, result);
    Object.entries(value).forEach(function([key, val]) {
      result[key] = deepClone(val, cache);
    })
    return result
  }
  if(Array.isArray(value)) {
    return value.map(item => cloneDeep(item, cache));
  }
}

Additionally, there's no sense in cloning a function so just return that back out. Handling instances of classes will need to be handled

Kevin Beal
  • 10,500
  • 12
  • 66
  • 92