I'm not a Javascript expert, but I've met a very strange situation that I can't explain. I'd like to see if some of you guys could help me understand this :-)
First, let's see the part that's working fine:
I want to setup three classes, one being the base class of the other ones, like this:
Base
|- Atom
|- Button
Here's the code I use:
//-------------------------------------------------------------------
function Base() {
this.values = {
a: true,
b: false
};
}
Base.prototype.display = function() {
for(i in this.values) {
window.console.log(i + ' : ' + this.values[i]);
}
window.console.log('---');
}
//-------------------------------------------------------------------
function Atom() {
this.values.a = false;
this.values.c = 'test';
}
Atom.prototype = new Base();
//-------------------------------------------------------------------
function Button() {
this.display(); // displays part (1)
this.atom = new Atom();
this.display(); // displays part (2)
this.atom.display(); // displays part (3)
}
Button.prototype = new Base();
//-------------------------------------------------------------------
new Button();
The "Base" class has a property named "values" which is a collection of values. The "display" method simply displays the "values" property to the console.
The "Atom" class inherits the "values" property from "Base", it just changes the value of "a" and adds a new "c".
The "Button" class has a property which is an object of the Atom class. It calls its "display" method before and after the creation of its "atom" property, then it also calls the "display" method for "atom" itself.
So far, so good. This is the result on screen :
a : true // part (1)
b : false
---
a : true // part (2)
b : false
---
a : false // part (3)
b : false
c : test
---
The first two sets of values (1) and (2) are from the Button class, the third (3) is from the Atom class, which has modified "a" and added "c".
The problem comes when I want to add an intermediate class between "Base" and its descendants, like this:
Base
|- Widget
|- Atom
|- Button
Here's the code with the new "Widget" class:
//-------------------------------------------------------------------
function Base() {
this.values = {
a: true,
b: false
};
}
Base.prototype.display = function() {
for(i in this.values) {
window.console.log(i + ' : ' + this.values[i]);
}
window.console.log('---');
}
//-------------------------------------------------------------------
function Widget() {
}
Widget.prototype = new Base();
//-------------------------------------------------------------------
function Atom() {
this.values.a = false;
this.values.c = 'test';
}
Atom.prototype = new Widget();
//-------------------------------------------------------------------
function Button() {
this.display(); // displays part (1)
this.atom = new Atom();
this.display(); // displays part (2)
this.atom.display(); // displays part (3)
}
Button.prototype = new Widget();
//-------------------------------------------------------------------
new Button();
As you can see, the base prototype for the "Atom" and "Button" classes has been changed to "Widget" instead of "Base".
That's where things become strange :
a : true // part (1)
b : false
---
a : false // part (2)
b : false
c : test
---
a : false // part (3)
b : false
c : test
---
The first set (before the creation of "atom") is unchanged. But the button's values will be changed right after the creation of the "atom", they are now the same as those of the "atom", which is not what I expected.
That's why I don't understand. When the atom is created, it modifies its own set of values, but why is the button's values modified as well ?
I thought "Button.values" and "Button.atom.values" would be two separate things, but it looks like they share their values, but they do that only if the intermediate "Widget" class is parent of both "Button" and "Atom".
That's very confusing to me... I hope my explanations are clear, do not hesitate to tell me if that isn't so.
Many thanks in advance for your time and answers :-) Marc.
values
refers to an object, and because of the way you're implementing pseudo-classical inheritance, there's only one object assigned to that property which ends up getting shared across prototypes. Your Widget
and Atom
both end up using the same object.
When setting up pseudo-classical inheritance with JavaScript, you see the pattern you're using a lot, but that's unfortunate because it has significant issues (including this one). Specifically, calling Base
to create the prototype Atom
(and so on) is a problem, because in general, constructor functions like Base
are designed to be used when constructing instances. The issue I usually flag up with them is: What if Base
needs to accept an instance-specific argument? What argument would you give it when creating the Atom
prototype?
Instead, don't call the constructor function until an instance is being constructed. To construct a prototype, create a new object that uses the parent "class"'s prototype as its underlying prototype, like this:
function Parent() {
}
function Child() {
Parent.call(this);
// Child initialization goes here
}
Child.prototype = Object.create(Parent.prototype); // Not `new Parent()`
Child.prototype.constructor = Child;
(If necessary, you can shim the single-argument version of Object.create
for pre-ES5 engines.)
The reason that would probably solve your problem is that using this pattern, the Base
function would get called when creating each instance, which means it creates a new object and assigns it to the values
property on that instance.
Side note: To save on verbosity and gain some features, I've created a small helper script called Lineage
for setting up pseudo-classical hierarchies. It simplifies the syntax, makes calls to super-"class" versions of methods easy, and various other things.