16

I am attempting to clone an object in JavaScript. I have made my own 'class' that has prototype functions.

My Problem: When I clone an object, the clone can't access/call any prototype functions.

I get an error when I go to access a prototype function of the clone:

clone.render is not a function

Can you tell me how I can clone an object and keep its prototype functions

This simple JSFiddle demonstrates the error I get: http://jsfiddle.net/VHEFb/1/

function cloneObject(obj) 
{
   // Handle the 3 simple types, and null or undefined
   if (null == obj || "object" != typeof obj) return obj;

   // Handle Date
   if (obj instanceof Date) {
     var copy = new Date();
     copy.setTime(obj.getTime());
     return copy;
   }

   // Handle Array
   if (obj instanceof Array) {
     var copy = [];
     for (var i = 0, len = obj.length; i < len; ++i) {
         copy[i] = cloneObject(obj[i]);
     }
     return copy;
   }

   // Handle Object
   if (obj instanceof Object) {
     var copy = {};
     for (var attr in obj) {
         if (obj.hasOwnProperty(attr)) copy[attr] = cloneObject(obj[attr]);
     }
     return copy;
   }

   throw new Error("Unable to copy obj! Its type isn't supported.");
}

function MyObject(name)
{
    this.name = name;
    // I have arrays stored in this object also so a simple cloneNode(true) call wont copy those
    // thus the need for the function cloneObject();
}

MyObject.prototype.render = function()
{
    alert("Render executing: "+this.name);
}

var base  = new MyObject("base");
var clone = cloneObject(base);
clone.name = "clone";
base.render();
clone.render();  // Error here: "clone.render is not a function"
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
sazr
  • 24,984
  • 66
  • 194
  • 362
  • i've seen [this solution](http://stackoverflow.com/a/728694/575527) here in SO. however, there is no clear way to exactly clone an object. [this one uses jQuery](http://stackoverflow.com/q/122102/575527) though – Joseph Apr 14 '12 at 05:21

4 Answers4

7

Some comments on the code:

>    if (obj instanceof Date) {
>      var copy = new Date();
>      copy.setTime(obj.getTime());

can be:

if (obj instanceof Date) {
  var copy = new Date(obj);

and

>    if (obj instanceof Array) {

will return false if obj is an array from another global context, such as an iFrame. Consider:

     if (o && !(o.constructor.toString().indexOf("Array") == -1))

>      var copy = [];
>      for (var i = 0, len = obj.length; i < len; ++i) {
>          copy[i] = cloneObject(obj[i]);
>      }

Copying the indexes of one array to another can be done more efficiently and accurately using slice:

      var copy = obj.slice();

though you will miss any other properties that might have been added that aren't numeric. Looping over 0 to length will add properties to the clone that don't exist in a sparse array (e.g. elisions will become undefined members).

As for the cloning part…

In the part copying object properties, that will copy all the properties, including those on the original's [[Prototype]] chain, directly to the "clone" object. The only way to really "clone" an object is to set its [[Prototype]] to the same object as the original, then copy the enumerable properties on the original (filtered with hasOwnProperty) to the clone.

The second part is trivial, the first part is not (in a general sense) since you can't guarantee that an object's constructor property references the object whose prototype is its [[Prototype]], nor can you guarantee that the constructor's prototype hasn't changed (i.e. is a different object) in the meantime.

The closest you can get is to use Lasse Reichstein Nielsen's clone (popularised by Douglas Crockford as beget) which makes the original object the [[Prototype]] of the clone, and then set the constructor to the same object. Though you probably still need to copy over the enumerable own properties so they mask the original's same-named properties.

So you can really only clone an object within a restricted context, you can't do it generally. And generally that realisation leads to a design where you don't need to generically clone objects.

RobG
  • 142,382
  • 31
  • 172
  • 209
  • Now I see (better) your point. Still I would be interested in references on prototype juggling. Have you one? – Eineki Apr 14 '12 at 07:11
  • ES5 introduced [Object.getPrototypeOf()](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/GetPrototypeOf) which could be used with `Object.create()`, but it's not well supported yet. Perhaps with feature testing for the obsoleted [\_\_proto__](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/Proto) you could cover a good number of browsers, but you'll miss pre-ES5 browsers such as IE 8 and earlier and zillion other minor user agents in phones, game consoles, etc. Check [Kangax's compat table](http://kangax.github.com/es5-compat-table/). – RobG Apr 14 '12 at 07:19
4

Instead of

var copy = {};

use

var copy = new obj.constructor;
Delan Azabani
  • 79,602
  • 28
  • 170
  • 210
  • Initially I suggested your same solution but, on a second thought i think this is not the way to go, What is the constructor changes his behaviour every time it is called think of a class that creates alternate rows of a table with different prototype chain. I know it is a bit stretched example but it is an option – Eineki Apr 14 '12 at 06:04
4

Your function can be simplified to:

function cloneObject(obj) 
{
   obj = obj && obj instanceof Object ? obj : '';

   // Handle Date (return new Date object with old value)
   if (obj instanceof Date) {
     return new Date(obj); 
   }

   // Handle Array (return a full slice of the array)
   if (obj instanceof Array) {
     return obj.slice();
   }

   // Handle Object
   if (obj instanceof Object) {
     var copy = new obj.constructor();
     for (var attr in obj) {
         if (obj.hasOwnProperty(attr)){
             if (obj[attr] instanceof Object){
                 copy[attr] = cloneObject(obj[attr]);
             } else {
                 copy[attr] = obj[attr];
             }
         }
     }
     return copy;
   }

   throw new Error("Unable to copy obj! Its type isn't supported.");
}

Here's a working jsfiddle

KooiInc
  • 119,216
  • 31
  • 141
  • 177
  • What happens if the constructor take arguments. How will this, `var copy = new obj.constructor();` play out? – seebiscuit Sep 23 '20 at 18:14
  • @seebiscuit Nothing. `new obj.constructor()` creates an _empty_ instance from the constructor of `obj` and assigns its properties from the `obj` properties. [Play with it in this JsFiddle](http://jsfiddle.net/KooiInc/snrL1zht/) – KooiInc Sep 24 '20 at 09:30
  • Right. The issue is that I can easily imagine a constructor where there's a dependency within the constructor on a reference passed in to the constructor. See this fork of your fiddle, for a simple example, http://jsfiddle.net/Seabiscuit/2wdaengo/1/. – seebiscuit Sep 29 '20 at 13:58
  • @seebiscuit Anyway, [took it a little further](http://jsfiddle.net/KooiInc/Lvqox1tf/), this cloning stuff – KooiInc Sep 29 '20 at 17:16
2

I would instance the clone object using the constructor of the object to be cloned:

 var copy = {};

will be

 var copy = new obj.constructor();

It is a quick response and I haven't pondered about drawbacks of such solution (I'm thinking of heavy constructor) but, at a first glance it should work (I wouldn't mention (or resort to) esoteric methods as __proto__).

Update:

you should resort to object.create to solve this problem.

 var copy = {};

will be

 var copy = Object.create(obj.constructor.prototype);

In this way the original constructor is not called to create the object (think of a constructor that does an lengthy ajax call to retrieve data from server) as Object.create is equivalent to

Object.create = function (proto) {  
    function F() {}  
    F.prototype = proto;  
    return new F();  
};  

And you can use this code if the javascript engine you are using does not support this function (it was added to the ecmascript 5 specs)

Eineki
  • 14,773
  • 6
  • 50
  • 59
  • The main issue is that `obj.constructor` may not reference the object that it was constructed from (e.g.using Crockford's `beget`, copied above). You can only be sure if you constructed the object in the first place, and in that case you know the constructor so why not reference it directly? – RobG Apr 14 '12 at 06:21
  • Oh, and the constructor's prototype may now be a different object so that `obj[[Prototype]] !== copy[[Prototype]]` even though they are from the same constructor. – RobG Apr 14 '12 at 06:52
  • @RobG I don't get the point, I want to clone an obj, create a blank one with desired prototype, and copy the original prototype to maintain the prototype chain. There is a method to change obj[[Prototype]] (I think it is a read only internal prop) so that it differs from obj.constructor.prototype? I would (really) know about such chance – Eineki Apr 14 '12 at 07:05