21

How do I clone a JavaScript class instance?

I tried the normal jQuery extend, but that just returns a vanilla object. I have looked through many other answers on stack, but could not find how to clone an instance.

function Parent(name) {
    this.name = name;
}

Parent.prototype.sayHello = function() {
    console.log('Hello my name is ' + this.name);
}

function Child(name) {
    Parent.call(this, name);
}

Child.prototype = Object.create(Parent.prototype);

var child = new Child('Billy');

var clone = $.extend(true, {}, child);
clone.name = 'Bob';

child.sayHello();
clone.sayHello();

console.log(child instanceof Child);
console.log(clone instanceof Child);

http://jsfiddle.net/39gjA/

I would prefer that the clone was deep/recursive. I.E. all properties that are objects are cloned as well.

Petah
  • 45,477
  • 28
  • 157
  • 213
  • So you want to make an exact copy, but one that doesn't create a reference to the original? If you want multiple objects that don't reference each other why don't you create the objects in a loop? – Ryan Apr 15 '13 at 21:32
  • 3
    This answer gives some excellent reasoning why this is going to be tough: http://stackoverflow.com/a/728694/187954 – Michael Robinson Apr 15 '13 at 21:46

6 Answers6

13

How do I clone a JavaScript class instance?

It's hardly possible if the instance was created with heavy use of closures in the constructor function. We may never now which internal values were set, and how to reproduce such a setup. Therefore, the easiest way would be if every class offered a clone function which knows what to do.

normal jQuery extend just returns a vanilla object

Not necessarily, it returns what you passed in. Use

var clone = $.extend(true, Object.create(Object.getPrototypeOf(child)), child);

instead and your instanceof usage will work fine. Note that the true signifies a "deep" copy which may or may not be what you want. Also, $.extend will happily copy enumerable inherited properties as well, so you might need to use a more sophisticated extend function.

Or without jQuery at all, and only copying own, enumerable properties and only using a shallow copy:

var clone = Object.assign(Object.create(Object.getPrototypeOf(child)), child);

But again, not all objects will be clonable in this way, see my first point above.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
10

This will create a copy of an object with the same prototype (class) and same own properties (including enumerability/writability/getters/setters etc):

function clone(obj) {
  return Object.create(
    Object.getPrototypeOf(obj), 
    Object.getOwnPropertyDescriptors(obj) 
  );
}

(see here)

It doesn't necessarily work well for builtin objects. For example Array.isArray(clone([])) is false, and clone(function () {})() says it is not a function, but for user created objects (either class instances or object literals) it works fine.

To do a deep clone, you will have to loop over the property descriptors and clone the values recursively:

function deepClone(obj) {
  if (obj === null || typeof obj !== "object")
    return obj
  var props = Object.getOwnPropertyDescriptors(obj)
  for (var prop in props) {
    props[prop].value = deepClone(props[prop].value)
  }
  return Object.create(
    Object.getPrototypeOf(obj), 
    props
  )
}
Jesse
  • 6,725
  • 5
  • 40
  • 45
2

You should try something like this:

function clone_object(o){
    var n=Object.create(
        Object.getPrototypeOf(o),
        Object.getOwnPropertyNames(o).reduce(
            function(prev,cur){
                prev[cur]=Object.getOwnPropertyDescriptor(o,cur);
                return prev;
            },
            {}
        )
    );
    if(!Object.isExtensible(o)){Object.preventExtensions(n);}
    if(Object.isSealed(o)){Object.seal(n);}
    if(Object.isFrozen(o)){Object.freeze(n);}

    return n;
}

Narrative:

  1. Create the new object using Object.create from a prototype and a properties object.
  2. For the prototype of the object to be cloned, use the prototype of the original object, using Object.getPrototypeOf.
  3. To create the properties object, loop over the own properties of the original object (using getOwnPropertyNames), and retrieve the property descriptor for each using getOwnPropertyDescriptor.
  4. Apply the extensibility/sealed/frozen characteristics of the original object to the clone.

This will not deep-clone properties whose values are themselves objects. That's left as an exercise to the reader...YMMV.

1

I think that not necessarily needs to work with classes(or functions instances), you could extend prototype to apply OOP. My suggestion is that you extend prototype instead of create classes. jQuery is for DOM but not for advance object treatment so while $.extend could be helpful in some cases for complex stuff there are more advanced libraries.

You could use libraries like CloneJS to easily work with extendable objects: https://npmjs.org/package/clonejs

Just include this script: http://quadroid.github.io/clonejs/cdn/clone.min.js

And try their own example:

/// Forget about classes.    
//  Instead of creating class (function), create prototype (object):
var $duck = $object.clone({
    name: 'Unnamed',
    quack: function(){
        console.log( this.name +' Duck: Quack-quack!');
    }
});
$duck.quack();//Unnamed Duck: Quack-quack!

/// Inheritance is simple: 
var $talkingDuck = $duck.clone({
    quack: function(){
        this.applySuper('quack');
        console.log('My name is '+ this.name +'!');
    }       
});

/// Forget about the `new` operator, use .create() method instead:
var donald = $talkingDuck.create({name: 'Donald'});
donald.quack();// Donald Duck: Quack-quack! My name is Donald!

/// Forget about the `instanceof` operator, use JS native 
//  .isPrototypeOf() method instead:
$duck.isPrototypeOf(donald);// true

Also I think that Backbone.js applies the extension of prototype instead of creation of classes. They use _.extend

Some more references: http://www.2ality.com/2011/11/javascript-classes.html http://underscorejs.org/#extend

Daniel Aranda
  • 6,426
  • 2
  • 21
  • 28
1

I'd be interested to see someone benchmark this method against other approaches to cloning class instances in JavaScript that use native JavaScript.

// Defining a class
function MyClass(args) {

  this.foo = "bar";
}
// Avoid duplicate instances of class methods in RAM by adding them to the class prototype like this
MyClass.prototype.method = method;
MyClass.prototype.clone = clone;

// Define what your methods do
function method() {

  console.log(this.foo);
}

function clone() {

  var classScope = this;

  // Get the prototype of your class. This will create an object that has one key for every method in your class. I'm not sure if this will go up the prototype chain if you have subclasses. Someone ought to edit this answer to clarify that.
  var miniMe = Object.getPrototypeOf(this);

  // Iterate the properties of your class--all the internal key-value pairs that do get duplicated in RAM each time you instantiate the class.
  Object.keys(this).forEach(iterate);

  function iterate(key, index, list) {

      // Add each property to your clone
      miniMe[key] = classScope[key];
    }
    // Return the clone
  return miniMe;
}


// Instantiate your class
var me = new MyClass();
// Clone it
var miniMe = me.clone();

// Run some tests
Object.keys(Object.getPrototypeOf(me)).forEach(iterate);
Object.keys(me).forEach(iterate);

function iterate(property, index, list) {

  if (!miniMe.hasOwnProperty(property))
    throw new Error("miniMe does not have property " + property);
}

// Change the value of miniMe.foo and make sure it didn't impact the value of me.foo
miniMe.foo = "baz";
if (me.foo === miniMe.foo)
  throw new Error("me.foo should not equal miniMe.foo, because we changed its value");
StudentsTea
  • 329
  • 4
  • 16
-1

Edited: When you know what kind of object you want to clone you can follow my example:

In your example you can simply do something like this:

var child = new Child('Billy');
var clone = new Child();
for (var prop in child) { 
  clone[prop] = child[prop];
}

I have updated your jsFiddle

nmoliveira
  • 1,759
  • 15
  • 14
  • Sorry, my example is simplified, and in my real code I don't know the constructor when I am wanting to clone. – Petah Apr 16 '13 at 00:43
  • In that case I would go with @Bergi's answer and do var clone = $.extend(true, Object.create(Object.getPrototypeOf(child)), child); – nmoliveira Apr 16 '13 at 20:01