HTML5 Zone is brought to you in partnership with:

Dr. Axel Rauschmayer is a freelance software engineer, blogger and educator, located in Munich, Germany. Axel is a DZone MVB and is not an employee of DZone and has posted 246 posts at DZone. You can read more from them at their website. View Full User Profile

A closer look at super-references in JavaScript and ECMAScript.next

11.30.2011
| 2516 views |
  • submit to reddit

This post examines how super-references work in JavaScript and how they will be simplified by ECMAScript.next. 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 me() {
        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, we 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 native feature (proposed by Allen Wirfs-Brock) of ECMAScript.next which allows one to write describe() much more succinctly:

    Employee.prototype.describe = function () {
        return super.describe()+" ("+this.title+")";
    };

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:
        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.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 ways of determining the value of super (step #1 above):

  • 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.next.
  • Static super-references: Each method that makes a super-reference has a property pointing to the object containing the method. That property can be set in either of two ways. First, declaratively: If the method is inside an object literal then ECMAScript.next automatically sets the property. Second, imperatively: The method Object.defineMethod() lets you add a method to an object and set the property at the same time. Afterwards, a method only needs access to itself to get the appropriate value for super. ECMAScript.next provides that access, but only internally, when a super-reference is made.

Simulating static super-references

To simulate ECMAScript.next super-references in ECMAScript 5, we need to store a reference from a method to its containing object and to refer to the current method from within a 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) {
        me.super.constructor.call(this, name);
        this.title = title;
    }
    Employee.prototype.describe = function me() {
        return me.super.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);
        addSuperReferencesTo(subProto);
        subC.prototype = subProto;
    };

    function addSuperReferencesTo(obj) {
        Object.getOwnPropertyNames(obj).forEach(function(key) {
            var value = obj[key];
            if (typeof value === "function" && value.name === "me") {
                value.super = Object.getPrototypeOf(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

 

 

Source: http://www.2ality.com/2011/11/super-references.html
Published at DZone with permission of Axel Rauschmayer, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)