2011-06-25

Prototypes as classes – an introduction to JavaScript inheritance

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.

Table of contents:

0. Executive summary
1. Introduction
     1.1. Prototypes
     1.2. Prototypes as classes
     1.3. Subclassing
     1.4. Super-calls
2. Classes in JavaScript (ECMAScript 5)
3. Comparing JavaScript and NextScript
4. ECMAScript.next: ensuring compatibility with legacy code
     4.1. Improved object literals in ECMAScript.next
5. A library for current JavaScript
6. Related reading

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

1. 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:
  • Sect. 1: Explain prototypes as classes via NextScript. First review the basics of prototypes (which are the same in JavaScript and NextScript). Then explain prototypes as classes in more detail.
  • Sect. 2: For comparison, show how current JavaScript does classes and subclassing – via constructor functions as classes.
  • Sect. 3: Show constructor functions as classes side by side with prototypes as classes to explain the benefits of the latter approach.
  • Sect. 4: ECMAScript.next might get prototypes as classes, in a way that maximizes compatibility with legacy code. ECMAScript.next will also have more powerful object literals, which would benefit prototypes as classes.
  • Sect. 5: The Proto.js library lets you use prototypes as classes in current JavaScript. Its succinctness is a partial validation of the approach.

1.1. Prototypes

The only inheritance mechanism that NextScript has is the prototype relationship between two objects. Prototypes work like in JavaScript:
  • The prototype chain: An object can optionally have a prototype, by pointing to another object via the internal property [[Prototype]]. The value of that property can be retrieved via Object.getPrototypeOf(). The prototype object can again have a prototype. The sequence of objects that are each other’s prototype is called the prototype chain.
  • Property lookup: If NextScript cannot find a property in an object, it keeps looking in the prototype, then the prototype’s prototype etc. While it does so, this always points to the beginning of the prototype chain.
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:

  • Instance and class, instance-of: the prototype object P of an object o plays the role of a class; o is an instance of P.
  • Subclass and superclass, subclass-of: a prototype object P can have a superclass P' by making the object P' its prototype.
The details of how this works are explained below.

1.2. 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:
  • Classes: A class C is an object. It contains data that is shared by all instances (mainly methods). A special method called “constructor” sets up the instance-specific data (mainly data properties).
  • Creating instances: Creating a new instance via new C(x,y) desugars to the following ECMAScript 5 code (internally):
        var o = Object.create(C);
        o.constructor(x,y);
    
    That is, the following happens:
    • Create a new object o whose prototype is C.
    • Call o.constructor(x,y)
    After these steps, o is an instance of its prototype object C. When looking for a property p via o.p, it should be obvious that both the unique instance data and the shared prototype data can be found, due to how prototypes work. If a method is found in the prototype and executed, the value of the this variable is still o, which enables the method to access the instance data.
  • Instance check:
        o instanceof C
    
    checks whether C is in the prototype chain of o. It is syntactic sugar for
        C.isPrototypeOf(o)
    
  • Getting the class: The class of an object is its prototype. Example: Are obj1 and obj2 (direct) instances of the same class?
        Object.getPrototypeOf(obj1) === Object.getPrototypeOf(obj2)
    
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.

1.3. Subclassing

A new class D extends an existing class C in two steps:
  • Inherit prototype properties: D’s prototype is C.
  • Inherit instance properties: D.constructor() calls C.constructor() before setting up its own instance data.
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:

1.4. 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:
  • Normal method lookup: obj.m(). To find the method, we look for the first object in the prototype chain of obj that has a property m and invoke that property’s value. During this invocation, this is bound to obj, the object where the search began.
  • Super-method lookup: super.l(). This invocation must be made from a method m. The search for l starts at the super-object of m (the prototype of the object that holds m). During the execution of l, this has the same value as it did in m.
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+")";
        }
    };

2. Classes in JavaScript (ECMAScript 5)

Let’s look at how classes work in JavaScript.
  • Classes: A class is a constructor function C. C.prototype points to an object with the instance methods. C itself sets up the instance data properties.
  • Creating instances: new C(x,y) does the following:
    • Create a new object o whose prototype is C.prototype.
    • Call C with this pointing to the newly created instance.
  • Instance check: o instanceof C checks whether C.prototype is in the prototype chain of o.
  • Getting the class: via the constructor property. Example: Are obj1 and obj2 (direct) instances of the same class?
        obj1.constructor === obj2.constructor
    
  • Subclassing: A new constructor D extends an existing constructor C in two steps:
    • Inherit prototype properties: Let D.prototype have the prototype C.prototype.
    • Inherit instance properties: Let constructor D call constructor C as a function (no new operator!) and hand in this so that C adds its properties to the new instance of D (without creating a new instance itself).
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 <|.

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

4. ECMAScript.next: ensuring compatibility with legacy code

  • Initial idea: My initial idea [1] is similar to the hypothetical NextScript as described above.
  • Ensuring compatibility: Afterwards, Allen Wirfs-Brock suggested how things could be adapted so that the existing “class protocol” wouldn’t have to be changed [2]. This proposal might make it into ECMAScript.next.

    Given a non-function object C (a “class object”, the prototype as a class):

    • Make sure that C.constructor.prototype points to C. This step is needed for the new operator to work as described below.
    • In the following two cases, treat non-function objects C differently, while not changing the behavior for functions:
      • Interpret new C(...) as syntactic sugar for new C.constructor(...).
      • Interpret o instanceof C as syntactic sugar for C.isPrototypeOf(o)
  • Subclassing old-style classes: It might make sense to let a new-style class inherit from an old-style class. There are two ways to do this:
    • Manually: The subclass extends Super.prototype. Constructor chaining and super-method calls should work as expected.
    • Automatically: Extend the prototype operator <| so that, when it encounters a function f as its left-hand side, it makes f.prototype the prototype and not f.
  • A competing proposal: Class literals have been proposed for ECMAScript.next. They look much like prototypes-as-classes, but are internally translated to constructor functions. It might be impossible to only have prototypes-as-classes in JavaScript (as that might break too much existing code). If so, then class literals avoid the hassle of having to support both prototypes-as-classes and constructor functions.
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:
  • It will be easier for IDEs to find your classes and support auto-completion etc.
  • It gives you the option to introduce more inheritance-related features in the future (e.g. traits).
  • They look familiar to people coming from class-based languages. Prototypes as classes ensure that the syntactic sugar is conceptually very similar to what is going on under the hood.

4.1. 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:
  • The prototype operator <| (borrowed by NextScript above).
  • Super references (also borrowed by NextScript).
  • A shorter notation for methods:
        var obj = {
            data: "abc",
            mymethod(x, y) {
                ...
            }
        };
    
  • Object literal shorthand: The following
        function f(x, y) { return {x, y}; }
    
    is syntactic sugar for
        function f(x, y) { return {x: x, y: y}; }
    

5. 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):
  • new operator – method new: Instead of
        new MyClass(...)
    
    you write
        MyClass.new(...)
    
    Credit for this idea goes to Irakli Gozalishvili and his prototype-centric inheritance library “selfish”. He comments:
    I think that the Proto.new(...) form is not a constraint. It’s a feature, as user may redefine their own custom “new”. I wish “new” and “instanceof” operators could be deprecated.
  • <| operator – method extend: Instead of
        A <| { ... }
    
    you write
        A.extend({ ... })
    
    Subclasses of Proto automatically inherit its class method extend, because Proto is part of their prototype chain.
  • instanceof operator – method isPrototypeOf: Instead of
        o instanceof C
    
    you write
        C.isPrototypeOf(o)
    
    This is a built-in JavaScript method. One just exploits the fact that the prototype of an instance is also its class.
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)'

6. Related reading

  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)

No comments: