2011-01-07

Going completely prototypal in JavaScript

Update 2011-06-25:Prototypes as classes” is an improved version of this blog post.

In this post we present the Proto API which implements inheritance in JavaScript in a purely prototypal fashion. This contrasts it with all other JavaScript inheritance APIs that the author is aware of.

To clarify what “purely prototypal” means, let us look at class inheritance and prototypal inheritance and how it compares to JavaScript:

  • Class inheritance works as follows: Classes are factories for objects. For methods and properties (attributes, fields), one specifies which ones are shared between instances (class methods and properties) and which ones appear in each instance (instance methods and properties). If we look closely, we see that instance methods are actually also shared between instances, but they act at the instance level.
  • Prototypal inheritance works as follows: You construct an initial instance and copy it for every new instance you need. For shared data and for instance methods, all copies have a common parent object (which JavaScript would call the prototype). The initial instances replace classes as creators of new instances. Thus, there are only ever objects and they are all manipulated directly.
  • JavaScript inheritance is an uneven mix of the previous two. The prototype is built directly, instance properties are put into a constructor which acts as an instance factory.
Consult [1] at the end of this post for details on JavaScript’s prototypal inheritance.

To improve JavaScript’s inheritance, most frameworks go the route of class inheritance and introduce class objects. For example, the following is Qooxdoo code:

qx.Class.define("twitter.MainWindow", {
    extend : qx.ui.window.Window,
    construct : function() {
        this.base(arguments, "twitter")
    }
});
Instead of introducing classes as a new mechanism, the Proto API presented in this post is purely prototypal. As a running example, the following code uses the API to create an initial person.
var person0 = Proto.extendWith({
    describe: function() {
        return "Person called "+this.name;
    }
}, {
    name: ""
}); 
person0 has a property name and a method describe(). The created prototype chain starts with the actual instance containing the property, continues with the prototype containing the method and ends with the proto-prototype which provides all the API functionality (such as extendWith()). Workers extend persons: they have a title and their describe method works differently. Accordingly, the initial worker instance worker0 builds on person0.
var worker0 = person0.extendProto({
    describe: function($super) {
        return $super() + " (" + this.title + ")";
    }
}, {
    title: ""
});
extendProto() extends both the prototype of person0 and the object itself. To do so, the prototype of person0 becomes the prototype of the first argument which in turn becomes the prototype of a new copy of person0. The second argument is then appended to this new copy (because here, we don’t have the option of a reference in JavaScript).
worker0 extends person0 by copying and adding to it. ProtoWorker extends ProtoPerson by pointing to it.
Initializing copies. If we add the following methods, then we can also initialize new copies as they are made.
var person0 = Proto.extendWith({
    init: function(name) {
        this.name = name;
    },
...

var worker0 = person0.extendProto({
    init: function($super, name, title) {
        $super(name);
        this.title = title;
    },
...
Interaction:
> var john = person0.copy("john");
> john.describe()
Person called john
> var jane = worker0.copy("jane", "manager");
> jane.describe()
Person called jane (manager)
With the Proto API, we have worked in a truly prototypal fashion and always created objects, not object factories. If this exercise is really useful remains to be seen, but it is interesting because of its conceptual purity and its closeness to JavaScript’s inheritance roots. You can download the complete source code as proto.js.

Related reading:

  1. Reflection and meta-programming in JavaScript (explains how to list the arguments of a function, a trick that is used in the Proto implementation)
  2. An easy way to understand JavaScript’s prototypal inheritance
  3. Lightweight JavaScript inheritance APIs

No comments: