12

I'm trying to figure out a way to deep clone a JS class instance while preserving all the prototypes down the chain.

I've seen how to deep clone an object:

JSON.parse(JSON.stringify(instance))

And I have seen how to make a shallow copy of a class instance:

Object.assign( Object.create( Object.getPrototypeOf(instance) ), instance)

But my question is, is there a way to deep clone an instance of a class?

Mendy
  • 7,612
  • 5
  • 28
  • 42
  • 1
    Are you trying to clone a class itself or an instance of a class? If you're trying to clone a class, please explain why and you can probably just subclass it. – jfriend00 Aug 18 '19 at 06:16
  • I'm trying to clone an instance, I'll edit my question to make it clear – Mendy Aug 18 '19 at 06:19
  • 4
    There is no foolproof way to clone all possible types of objects in JS, particularly if it contains references to other objects. The best way is to support a `.clone()` method (or name it whatever you want) on the object itself and have the object support making a duplicate of itself. Then, it can do the right thing with any instance data which only the object implementation itself can know how to handle all possible types of instance data. – jfriend00 Aug 18 '19 at 06:24
  • 3
    You shouldn't rely on JSON.parse and stringify to clone objects that are not simple data structures (even then, it's **slow**). The JSON process computes a one-time value for getters instead of cloning the definition, ignores non-enumerable properties and symbols, prototypes (as you've noticed), and makes no effort at all to handle circular references. That can be improved with the [optional argument to JSON.parse](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#Using_the_reviver_parameter) and the corresponding one on JSON.stringify. – Touffy Aug 18 '19 at 06:54

3 Answers3

12

There is no foolproof way to clone all possible types of objects in JS, particularly if it contains references to other objects. A generic cloning argument doesn't know whether an object reference in the clone should contain the same reference (such as a common parent) or whether it needs to clone the object I have a reference too also. It isn't possible to know that generically as it really depends upon the implementation of the object.

If there are circular references to objects such as parent to child and child to parent, it gets even more complicated.

As another example, imagine an object that as part of its constructor, it creates a unique object ID, registers that id with some service and then stores the ID in its instance data. There's no way for a generic clone mechanism to know that logic (generating a new ID and registering it with some service) is required to make a new object. That type of logic would have to be done by code specific to that object that knows what to do.

As another example, a constructor might create closures (with access to private information) that there is no way to duplicate from the outside.

As another example, a constructor might bind methods to its own instance that a generic clone would have no idea it needed to do.

The best way to clone an object is with code built into the implementation of the object that knows how to clone itself such as adding a .clone() method (or name it whatever you want) on the object itself and have the object support making a duplicate of itself. Then, it can do the right thing with any instance data which only the object implementation itself can know how to handle all possible types of instance data.

Alex
  • 1,457
  • 1
  • 13
  • 26
jfriend00
  • 683,504
  • 96
  • 985
  • 979
7

I recommend using Lodash's cloneDeep. It works with all types, function and Symbol are copied by reference.

Using the JSON.parse(JSON.stringify(myObject)) way is problematic when there's circular reference. Also, it replaces the object's methods to undefined and reorders the properties.

Alex Pappas
  • 2,377
  • 3
  • 24
  • 48
  • this should be the answer here, no need to reinvent the wheel, just use Lodash's cloneDeep, works like a charm. And background is quite simple, for a real new Object you need to loop through all keys of an object copy it to the new and also copy the values, if you don't want to miss something – xzesstence Jan 28 '20 at 13:27
4

A possible hack is to invoke the structured clone algorithm to make the clone by sending the instance through a MessageChannel, like this:

function deepClone(instance) {
    return new Promise(resolve => {
        const messageChannel = new MessageChannel();
        messageChannel.port2.onmessage = e => resolve(e.data);
        messageChannel.port1.postMessage(instance);
    });
}

This will handle types like Map, Set, Date, RegExp, Blob, ArrayBuffer and more, and it can even clone custom classes.

Some other ways to trigger a structured clone can be found here.

Mendy
  • 7,612
  • 5
  • 28
  • 42
  • 1
    I like the idea of using structured clone. A less hacky and actually fast solution would be to just implement it. It's well enough specified that it shouldn't be much trouble. – Touffy Aug 18 '19 at 06:57
  • 1
    @Touffy do you know of any JavaScript implementations? This sounds like a good idea – Mendy Aug 18 '19 at 07:00