2011-11-29

A closer look at super-references in JavaScript and ECMAScript 6

Update 2013-04-09: now simulates the approach of the ECMAScript 6 specification draft (search for "HomeObject" to find the relevant parts).

This post examines how super-references work in JavaScript and how they will be simplified by ECMAScript 6. To understand this post, it helps to be familiar with JavaScript inheritance. If you are not, consult [2].

Extending constructors in JavaScript

Let’s look at the following JavaScript code where the constructor Employee extends the constructor Person. The extension is performed via a custom function inherits() (whose code will be shown later).

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

    // Sub-constructor
    function Employee(name, title) {
        Person.call(this, name);
        this.title = title;
    }
    Employee.prototype.describe = function () {
        return Person.prototype.describe.call(this)+" ("+this.title+")";
    };
    inherits(Employee, Person);
Employee is used as follows:
    > var jane = new Employee("Jane", "CTO");
    > jane.describe()
    'Person called Jane (CTO)'

Super-references

To understand how Employee.prototype.describe invokes its super-method, let’s take a look at the structure of the instance jane:

jane is the first member in a chain of prototypes. Its direct prototype is Employee.prototype whose prototype is Person.prototype. Super-references (including super-calls) are a feature of ECMAScript 6 which allows one to write describe() much more succinctly:

    Employee.prototype.describe = function () {
        // super() is an abbreviation of super.describe()
        return super()+" ("+this.title+")";
    };
Although they look similar, super and this are independent features. super means “I’m currently in a method – find the method that that method has overridden and apply it to the same instance that is presently active (i.e., this stays the same)”. this staying the same is needed in the example, otherwise the overridden describe() wouldn’t be able to access this.name. Hence, this is about the instance one is currently modifying, while super is about finding an overridden method in the prototype chain. In JavaScript, “y overrides x” just means “y is found before x in the prototype chain”.

Lets put the above intuitive description into an algorithm: To make the super-call super.describe(), the following steps are performed:

  1. Determine super, the prototype of the object in which the current method is located.
  2. Search for describe: start at super, traverse the prototype chain until you find an object that has a property describe, return the value of that property.
  3. Call the function you have found, but leave this as it was before. Rationale: the overridden version of describe() that is to be invoked needs to be able to access jane’s properties.
Taking this into consideration, we see that super.describe() is correctly implemented by
    Person.prototype.describe.call(this)
All the steps are performed:
  1. Determine super (hard-coded):
        Person.prototype
    
  2. Search for describe.
        Person.prototype.describe
    
    Note that we also find a describe if there isn’t one in Person.prototype directly, but in one of its prototypes.
  3. Execute the method, but keep the current this:
        Person.prototype.describe.call(this)
    
The prototype object has a property constructor pointing to the constructor [3]:
    Employee.prototype.constructor === Employee
That allows the constructor Employee to access its super-constructor like a super-method:
    function Employee(name, title) {
        super(name);  // abbreviation of super.constructor(name)
        this.title = title;
    }
Caveat: The above only works with static super-references (see below for details). Note that the semantics of super-references only depends on a prototype chain being there and on the ability to determine the object that holds the current method. It does not matter how the prototype chain has been created: via a constructor, via an object exemplar (a prototype as a class [2]), or via Object.create(). Super-references are not limited to subtyping, either: One can just as well override a method in the prototype via a method in the instance and have the latter call the former.

Determining super

There are two approaches for determining the value of super (step #1 above).

First, dynamic super-references. When you look for a method you let it know in which object you found it, similar to how this is handed to a method. When resolving a super-reference, the value of super is the prototype of that object. The drawback of this approach is that it incurs runtime costs for all methods, not just for those that are making super-references. These costs prevent dynamic super-references from being viable for ECMAScript 6.

Second, static super-references. The home object of a method is the object it is stored in. super is always the prototype of the home object of the current method. To enable static super-references, ECMAScript 6 gives each relevant method an internal property [[HomeObject]] pointing to the method’s home object. That property’s value can be accessed while the method is executed, in order to compute super. There are three ways to set up [[HomeObject]]:

  • Declaratively: Methods in in an object literal have a special, more compact syntax in ECMAScript 6. If you use that syntax, [[HomeObject]] will be set up automatically.
  • Declaratively: If you use ECMAScript 6 classes [5], [[HomeObject]] will be set up automatically.
  • Imperatively: If you add methods to an object via Object.assign() or Object.mixin() [6] then those functions ensure that [[HomeObject]] is configured appropriately.

Simulating static super-references

To simulate ECMAScript 6 super-references in ECMAScript 5, we need to store a reference from a method to its home object and to refer to the current method from within that method. The former can be done by inherits(). The latter used to be possible via arguments.callee, but that property has been deprecated and is illegal in strict mode [4]. The alternative is a named function expression – you can give a function expression a name. It then looks like a function declaration, but is still an expression, not a statement:
    var fac = function me(x) {
        if (x <= 0) {
            return 1
        } else {
            return x * me(x-1)
        }
    };
The function on the right-hand side of the assignment can refer to itself via me, independent of the variable that it has been assigned to. The identifier me only exists inside the function body:
    > (function me() { return me }());
    [Function: me]
    > me
    ReferenceError: me is not defined
There can thus be several functions within the same scope that all use the name me.
    var Employee = function me(name, title) {
        ssuper(me).constructor.call(this, name);
        this.title = title;
    }
    Employee.prototype.describe = function me() {
        return ssuper(me).describe.call(this)+" ("+this.title+")";
    };
inherits() works as follows (see gist for complete source code):
    function inherits(subC, superC) {
        var subProto = Object.create(superC.prototype);
        // At the very least, we keep the "constructor" property
        // At most, we preserve additions that have already been made
        copyOwnFrom(subProto, subC.prototype);
        setUpHomeObjects(subProto);
        subC.prototype = subProto;
    };
    function ssuper(func) {
        return Object.getPrototypeOf(func.__homeObject__);
    }

    function setUpHomeObjects(obj) {
        Object.getOwnPropertyNames(obj).forEach(function(key) {
            var value = obj[key];
            if (typeof value === "function" && value.name === "me") {
                value.__homeObject__ = obj;
            }
        });
    }

    function copyOwnFrom(target, source) {
        Object.getOwnPropertyNames(source).forEach(function(propName) {
            Object.defineProperty(target, propName,
                Object.getOwnPropertyDescriptor(source, propName));
        });
        return target;
    };

Related reading

  1. Object Initializer super references
  2. Prototypes as classes – an introduction to JavaScript inheritance
  3. What’s up with the “constructor” property in JavaScript?
  4. JavaScript’s strict mode: a summary
  5. ECMAScript.next: classes
  6. ECMAScript.next: TC39’s September 2012 meeting

No comments: