Prototypes as classes – an introduction to JavaScript inheritance (original) (raw)

Updates – read first:

JavaScript’s prototypal inheritance is hard to understand, especially for people coming from other languages that are used to classes. This post explains that it does not have to be that way: The proposal “prototypes as classes” is a simplification of classes and inheritance in JavaScript. It might become part of ECMAScript.next, a.k.a. “the next version of JavaScript” (after ECMAScript 5). But there is also a library that allows you to use its features in today’s JavaScript. What’s intriguing about prototypes as classes is that they aren’t a radical departure from current practices, but rather a clarification of them.

Incidentally, this post is also a good introduction to JavaScript inheritance, because the basics are easier to understand with prototypes as classes.

Executive summary: The core idea of “prototypes as classes” is that prototypes are better classes than constructor functions. That is, if you have a class C and create instances via new C(), then C should be the prototype of the instances, not their constructor function. Sect. 3 shows constructor functions as classes side by side with prototypes as classes, which should make it obvious which one is the better choice. Sect. 5 presents a library that lets you use prototypes as classes in today’s JavaScript.

Read on for a gentle introduction to this idea.

Introduction

Prototypes as classes are explained by introducing a hypothetical programming language called “NextScript”. That language is exactly like JavaScript, with one difference: Where JavaScript uses the relatively complicated constructor functions to create instances, NextScript uses prototypes to do so. That is, prototypes are classes in NextScript. This post is structured as follows:

Prototypes

The only inheritance mechanism that NextScript has is the prototype relationship between two objects. Prototypes work like in JavaScript:

The prototype operator. NextScript has a new prototype operator

<|

for specifying the prototype of an object literal [4]:

var obj = A <| { foo: 123 };

After the above assignment,

obj

has the prototype

A

. The equivalent in JavaScript is

var obj = Object.create(A);
obj.foo = 123;

Prototypes do everything. Both the instance-of relationship and the subclass-of relationship is expressed via the has-prototype relationship in NextScript:

The details of how this works are explained below.

Prototypes as classes

When you think of a class as a construct that produces instances, then the closest thing to a class that JavaScript has are constructor functions (Sect. 2). In contrast, NextScript uses plain old (non-function) objects:

You can see that while NextScript’s classes are only objects, there is not much difference between them and “real” classes in other programming languages (such as Python, Java, or C#). The following example shows the NextScript class

Person

in action.

var Person = {
    constructor: function (name) {
        this.name = name;
    },
    describe: function() {
        return "Person called "+this.name;
    }
};

Interaction:

> var john = new Person("John");
> john.describe()
Person called John
> john instanceof Person
true


All instances of

Person

have that class as a prototype.

Subclassing

A new class

D

extends an existing class

C

in two steps:

Thus, we have performed everything that is necessary to make

D

a subclass of

C

: Instances of

D

will have their own instance data in addition to

C

’s instance data. And they will have

D

’s methods in addition

C

’s.

Example: Subclass Worker extends superclass Person.

var Worker = Person <| {
    constructor: function (name, title) {
        Person.constructor.call(this, name);
        this.title = title;
    },
    describe: function () {
        return Person.describe.call(this)+" ("+this.title+")"; // (*)
    }
};

Interaction:

> var jane = new Worker("Jane", "CTO");
> jane.describe()
Person called Jane (CTO)
> jane instanceof Worker
true
> jane instanceof Person
true

The instance

jane

has the prototype

Worker

which has the prototype

Person

. That is, prototypes are used for both the instance-of relationship and the subclass-of relationship. The following diagram shows this prototype chain:

Super-calls

Things become even simpler if we add one more feature: super-calls [4]. They make it easier for an overriding method to call the method it overrides (its super-method). There are two ways of looking up methods:

In other words: A super-method call is the same as a

this

-method call, only the search for the property starts later in the property chain. The tricky thing with super-method lookup is to find the super-object. This can be done manually, by directly naming it, as in method

Worker.describe()

at (*). Or it can be performed automatically, via a language construct of NextScript:

var Super = {
    foo: ...
};
var Sub = Super <| {
    foo: function (x, y) {
        super.foo(x, y); // (**)
    }
};

The statement at (**) is a super-method lookup and syntactic sugar for

Object.getPrototypeOf(Sub).foo.call(this, x, y);

Now

Worker

can be simplified as follows.

var Worker = Person <| {
    constructor: function (name, title) {
        super.constructor(name);
        this.title = title;
    },
    describe: function () {
        return super.describe()+" ("+this.title+")";
    }
};

Classes in JavaScript (ECMAScript 5)

Let’s look at how classes work in JavaScript.

Example: We replace the default prototype and thus have to set

Worker.prototype.constructor

[3].

// Superclass
function Person(name) {
    this.name = name;
}
Person.prototype.describe = function() {
    return "Person called "+this.name;
};

// Subclass
function Worker(name, title) {
    Person.call(this, name);
    this.title = title;
}
Worker.prototype = Object.create(Person.prototype);
Worker.prototype.constructor = Worker;
Worker.prototype.describe = function() {
    return Person.prototype.describe.call(this)+" ("+this.title+")";
};

Note that super-calls are orthogonal to new-style inheritance and would be just as useful in the above code. The same holds for the prototype operator

<|

.

Comparing JavaScript and NextScript

The new approach allows you to work more directly with the core inheritance mechanism of JavaScript – prototypes. It thus continues the tradition of Crockford’s prototypal inheritance (

Object.create()

in ECMAScript 5). Several aspects of JavaScript become conceptually clearer with prototypes as classes/NextScript:

  1. Being an instance versus having a prototype: In JavaScript, an instance o has two relationships with its class C: o is the instance of C and has the prototype C.prototype. In NextScript, there is only the prototype relationship between instance and class. As a result, instanceof becomes easier to understand.
    JavaScript: o instanceof C === C.prototype.isPrototypeOf(o)
    NextScript: o instanceof C === C.isPrototypeOf(o)
  2. Subclassing: In JavaScript, there is an indirection involved in subclassing. To let constructor D subclass constructor C, you must make D.prototype the prototype of C.prototype. In NextScript, you directly connect a subclass to its superclass. As a result, it is also easier to determine whether one class is a subclass of another one.
    JavaScript: Sub.prototype = Object.create(Super.prototype)
    NextScript: Sub = Object.create(Super)
  3. Checking for a superclass relationship: In JavaScript, a super-constructor and a sub-constructor are only related via the values of their prototype properties. Prototypes as classes are directly related.
    JavaScript: Super.prototype .isPrototypeOf(Sub.prototype)
    NextScript: Super .isPrototypeOf(Sub)
  4. Super-calls: When calling an overridden method in a superclass, you access the method in the super-prototype in JavaScript (i.e, not the superclass).
    JavaScript: Super.prototype.foo.call(this)
    NextScript: Super.foo.call(this)
  5. Inheriting class methods: In JavaScript, if a class has a method then a subclass does not inherit it. In NextScript, class methods are automatically inherited, due to the prototype relationship.
  6. Instantiation versus initialization: When it comes to creating a new instance, there are two concerns:
    1. Instantiation: Create a new instance, give it the proper prototype.
    2. Initialization: Set up the instance variables.
      In JavaScript, a constructor function either plays both roles or just role #2 (when called from a sub-constructor). In NextScript, the method constructor() is only responsible for initialization (it could be renamed to initialize to make that fact more explicit). As a result, initialization chaining in NextScript is conceptually simpler than constructor chaining in JavaScript.
  7. Generic methods: To use a generic method, you have to refer to a prototype. The following example shows how to turn the pseudo-array arguments in an array via the slice() method of class Array.
    JavaScript: Array.prototype.slice.call(arguments)
    NextScript: Array.slice.call(arguments)

If you look at the JavaScript code above, you will notice that, after instantiation, we only need the constructor to access the prototype. This makes it obvious that the prototype should be the class and not the constructor.

ECMAScript.next: ensuring compatibility with legacy code

Note that the internal structure is still the same as before. The only difference is that the variable that names the class refers to the prototype and not the constructor. This should make it clear why the proposal is called “prototypes as classes”. And that it changes relatively little.

Alas, as things stand right now, it is not likely that prototypes as classes will ever make it into JavaScript. My current favorite are class literals that desugar to prototypes-as-classes, e.g.

class Foo extends Bar {
   ...
}

This would produce a prototype-as-class called

Foo

and the “class body” in curly braces would be very similar to an object literal. Class literals give you three benefits:

Improved object literals in ECMAScript.next

The proposal “Object Literal Extensions” has been accepted for ECMAScript.next. It is essential for making “prototypes as classes” easy to use. Highlights:

A library for current JavaScript

The following code implements “prototypes as classes” in ECMAScript 5 and can be downloaded at proto-js on GitHub. Current JavaScript does not let you do prototypes-as-classes as shown above. Thus, the library uses methods instead of the following three operators (for which you cannot provide a custom implementation):

The library:

// To be part of ECMAScript.next
if (!Object.getOwnPropertyDescriptors) {
    Object.getOwnPropertyDescriptors = function (obj) {
        var descs = {};
        Object.getOwnPropertyNames(obj).forEach(function(propName) {
            descs[propName] = Object.getOwnPropertyDescriptor(obj, propName);
        });
        return descs;
    };
}

/**
 * The root of all classes that adhere to "the prototypes as classes" protocol.
 * The neat thing is that the class methods "new" and "extend" are automatically
 * inherited by subclasses of this class (because Proto is in their prototype chain).
 */
var Proto = {
    /**
     * Class method: create a new instance and let instance method constructor() initialize it.
     * "this" is the prototype of the new instance.
     */
    new: function () {
        var instance = Object.create(this);
        if (instance.constructor) {
            instance.constructor.apply(instance, arguments);
        }
        return instance;
    },

    /**
     * Class method: subclass "this" (a prototype object used as a class)
     */
    extend: function (subProps) {
        // We cannot set the prototype of "subProps"
        // => copy its contents to a new object that has the right prototype
        var subProto = Object.create(this, Object.getOwnPropertyDescriptors(subProps));
        subProto.super = this; // for super-calls
        return subProto;
    },
};

Using the library:

// Superclass
var Person = Proto.extend({
    constructor: function (name) {
        this.name = name;
    },
    describe: function() {
        return "Person called "+this.name;
    }
});

// Subclass
var Worker = Person.extend({
    constructor: function (name, title) {
        Worker.super.constructor.call(this, name);
        this.title = title;
    },
    describe: function () {
        return Worker.super.describe.call(this)+" ("+this.title+")";
    }
});

Interaction:

var jane = Worker.new("Jane", "CTO"); // normally: new Worker(...)

> Worker.isPrototypeOf(jane) // normally: jane instanceof Worker
true

> jane.describe()
'Person called Jane (CTO)'
  1. Classes: suggestions for improvement [Initial idea to allow new for non-function objects]
  2. Prototypes as the new class declaration [Proposal for ensuring the compatibility with the current protocol]
  3. What’s up with the “constructor” property in JavaScript?
  4. Harmony: Object Literal Extensions
  5. Lightweight JavaScript inheritance APIs [Especially Resig’s Simple Inheritance looks almost like NextScript]
  6. A brief history of ECMAScript versions (including Harmony and ES.next)