Shared Prototype

The JavaScript Inheritance Problem

JavaScript is an object-oriented language, but several of its design characteristics are significantly different from common modern object-oriented languages, like Java or C#. The key issue is JavaScript's "prototype-based inheritance" where a class is derived from an instance of an object, state and all, rather than from a base class.

From the ECMAScript specification (emphasis added): "In a class-based object-oriented language, in general, state is carried by instances, methods are carried by classes, and inheritance is only of structure and behavior. In ECMAScript, the state and methods are carried by objects, and the structure behavior and state are all inherited."

The best way to explain this is to offer an example. Say we define a class, Alpha, that has an a single instance variable, an array called stuff:

Alpha = function() {
    this.stuff = new Array();
};

Alpha.prototype.addStuff = function(item) {
    this.stuff.push(item);
};

Then we create a couple of instances and add an element to each of their array properties:

var instance1 = new Alpha();
instance1.addStuff("foo");

var instance2 = new Alpha();
instance2.addStuff("bar");

alert("ONE: " + instance1.stuff + ", TWO: " + instanceTwo.stuff);

The expected message is displayed: "ONE: foo, TWO: bar".

Now lets create a derivative object that extends Alpha:

Beta = function() {
};

Beta.prototype = new Alpha();

And again, we'll create two instances of Beta, and call the same method as before:

var instance3 = new Beta();
instance3.addStuff("foo");

var instance4 = new Beta();
instance4.addStuff("bar");

alert("THREE: " + instance3.stuff + ", FOUR: " + instance4.stuff);

This time we get a different answer: "THREE: foo, bar, FOUR: foo, bar".

This is of course, by design. We did not extend a class called Alpha, we extended an instance of Alpha (more specifically, we made the prototype of Beta an instance of Alpha). That prototype instance of Alpha has a single property called stuff. Every implementation of Beta has a property called stuff that references that one single array.

We can somewhat get around this issue by calling the constructor of Alpha from Beta using Function.call(), as in this implementation:

Beta = function() {
    Alpha.call(this);
};

Beta.prototype = new Alpha();

This is still far from an ideal solution. While it works in this case, you're still imposing a major design constraint on the base class constructor. The base constructor needs to be capable of both constructing an instance of the base class and a prototype for derivative objects. This is not a recipe for maintainable code.

If you fail to call the constructor, you won't get an error. Instead, you'll get to stay up all night trying to figure out why everything goes crazy once you create a second instance of the object.

What we really want here is the capability to extend a class, not an instance.

The Shared Prototype

There is a solution to this problem. We can create a new class that has an empty constructor that shares its prototype with the base class. Derivative classes can then be derived from an instance of the empty-constructor-version.

Alpha = function() {
    this.stuff = new Array();
};

Alpha.prototype.addStuff = function(item) {
    this.stuff.push(item);
};

// To create a derived class from Alpha:
Beta = function() {
    Alpha.call(this);
};

var def = function() { };
def.prototype = Alpha.prototype;
Beta.prototype = new def();

// To create an instance of Alpha:
var a = new Alpha();

We're still extending an instance of an object of course, but this time that object has no state. The net effect is traditional class-based inheritance. The instanceof operator still works as the objects share a prototype, and instanceof determines inheritance based on the prototype chain.

The syntax isn't particularly desirable though. This issue is corrected when using Core.extend(), where the above work is done behind the scenes automatically:

Alpha = Core.extend({

    $construct: function() {
        this.stuff = new Array();
    },
    
    addStuff: function(item) {
        this.stuff.push(item);
    }
});

Beta = Core.extend(Alpha, {

    $construct: function{
        Alpha.call(this);
    }
});

More information on how Core.extend() works is provided in later chapters.