Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to generate new instances of an object without the class?

Tags:

javascript

Question:

How can I generate new instances of a class from an instance of that class?

What I've found:

Consider the next class and its instance:

// Create a new class
var Foo = function Foo () {
  console.log('New instance of FOO!');
};

Foo.prototype.generate = function(first_argument) {
  this.arr = [1,2,3,4,5];
};

var foo = new Foo();

foo.generate();
console.log(foo.arr); // [1,2,3,4,5]

Note that foo is as an instance fo Foo, and i'll be using this instance in the examples.

Object.create

With Object.create I can generate new copies of the foo instance, but they will share the state data:

var bar = Object.create(foo);
console.log(bar.arr); // [1,2,3,4,5]

bar.arr.push(6);
console.log(foo.arr); // [1,2,3,4,5,6]
console.log(bar.arr); // [1,2,3,4,5,6]

And if some logic is in the constructor it is not called.

Prototype inheitance

This is similar to the object create but it calls the constructor. It still has the same state data problem:

var Bar = function () {
  foo.constructor.call(this);
};

Bar.prototype = foo;

var bar = new Bar();
console.log(bar.arr); // [1,2,3,4,5]

bar.arr.push(6);
console.log(foo.arr); // [1,2,3,4,5,6]
console.log(bar.arr); // [1,2,3,4,5,6]

Firefox proto

Here is an example of what I exactly want to do, but it only works in Firefox:

// Create Bar class from foo
var Bar = foo.constructor;
Bar.prototype = foo.__proto__; // Firefox only

Here I have the class Bar that's a copy of the class Foo and I can generate new instances.

var bar = new Bar();

console.log(bar.arr); // undefined

bar.generate();
console.log(bar.arr); // [1,2,3,4,5]

Is there any other way to achive the same goal that works in all the browsers?

like image 726
Kaizo Avatar asked Jan 12 '23 23:01

Kaizo


2 Answers

1) proto has to be avoided for obvious reasons, you have Object.getPrototypeOf to read an object's prototype in a standard way.
2)You don't have the constructor doing all the job, but rather have a kind of init method with generate. So it would make much more sense to have a call to this.generate at the end of the constructor function. This is why prototype inheritance doesn't work : the constructor does not its job. In the same idea, you cannot blame Object.create since it does not neither the constructor, nor generate in your example, so you basically build an object having same prototype, that's all.

Provided you do add a call to this.generate to the constructor function, maybe the most easy way is to use the constructor property of foo to build a new object :

var bar = new foo.constructor();

which will give you a fresh new item.

So to answer your question : yes there's another way, using the constructor property of the object instance to build a new item.

But no inheritance scheme will be able to 'guess', that the full initialisation should be performed with such and such method. Initialisation is the job of the constructor. We might imagine for instance that you buil a new array this.arr = []; in the constructor and that generate() will push() items inside this array, this would be a valid way. But you have to avoid adding properties through method, both for optimisation and clarity : setup all instance properties within the constructor, and maybe shared properties on the prototype, and only use those properties in the methods.

like image 108
GameAlchemist Avatar answered Feb 03 '23 11:02

GameAlchemist


In light of your comment to Craig's answer: creating a new instance from any object can be easily done in a single line:

var newFoo = new (Object.getPrototypeOf(foo).constructor);

For some older browsers, Object.getPrototypeOf won't work, though. You can easily fix this by adding this bit to your script:

if (!Object.getPrototypeOf)
{
    Object.getPrototypeOf = function(o)
    {
        if (o instanceof Object)
        {
            return (o.__proto__ || o.prototype);
        }
        throw new TypeError('Object.getPrototypeOf called on non-object');
    };
}

All things considered, in most cases:

var newFoo = new (foo.constructor);

works just fine, unless some joker messed with the instance, assigning it a constructor property, or changed the prototype.

It looks to me as if you don't fully understand how JS resolves the context, and how references to properties of objects are resolved. Check the diagram in my answer here.

First off, change your last snippet to this:

Bar.prototype = Foo.prototype;//__proto__ is non-standard proto reference
Bar.prototype.constructor = Bar;

That should work X-browser.
As to why your other attempts failed, that's quite easy: when you write bar.arr, JS simply looks for an arr property directly attached to the instance. If that isn't found, the search is expanded to the Bar.prototye. When that fails to, JS searches the prototype of Bar.prototype, which is foo (the instance passed to Object.create, for example).
There, JS does find the property it was looking for, therefor bar.arr references foo.arr. If you change the object (Arrays are objects), it doesn't matter which reference was used to access the object. There is but one instance, which is kept in memory for as long as it is referenced at least once.

In full:

var Foo = function()
{
    console.log('the Foo constructor');
};
Foo.prototype.generate = function()
{
    this.arr = [1,2,3];
    return this;
};
var Bar = function()
{
    console.log('the Bar constructor');
};
Bar.prototype = new Foo;//make Bar inherit from Foo
Bar.prototype.constructor = Bar;//but make it use its own constructor

Now you can use both Foo and Bar as you wanted:

var foo = new Foo;
foo.generate();
foo.arr.push(4);
var bar = new Bar;
bar.generate();
console.log(foo.arr);//here [1,2,3,4]
console.log(bar.arr);//[1,2,3]

Of course, if you want to Bar to start off with a particular instance of foo:

var foo = new Foo;
foo.generate();
Bar.prototype = foo;//
Bar.prototype.constructor = Bar;
var bar = new Bar;
console.log(bar.arr);//logs [1,2,3]
//BUT BE CAREFUL:
console.log(foo.arr === bar.arr);//true, bar only references the same property
bar.arr = bar.arr.slice(0);//copies the array
bar.arr.push(4);
console.log(foo.arr);//[1,2,3] still
console.log(bar.arr);//[1,2,3,4] changed

To undo the changes I've done above, I need only to unmask the prototype propery:

delete bar.arr;
console.log(bar.arr);//back to [1,2,3]
console.log(bar.arr === foo.arr);//true, I've deleted a bar property

That's it

like image 44
Elias Van Ootegem Avatar answered Feb 03 '23 13:02

Elias Van Ootegem