CoreJS.SharedPrototype

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 have an object "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

The solution to this problem is to create an object with an empty constructor that is used to create an object prototype, and then share that prototype with an object that has a real constructor. When you need to create a derivative object, you extend the empty-constructor version. When you need to create an instance, you call the real-constructor version.

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

AlphaDef = function() { };
Alpha.prototype = AlphaDef.prototype;

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

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

Beta.prototype = new AlphaDef();

// 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.

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);
    }
});

last edited 2007-10-30 08:57:06 by TodLiebeck