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

11 comments:

Mariusz Nowak said...

This is indeed most closest you can get to ES5. In my opinion 'super' is the only missing thing from JavaScript OOP.
On my side I use special version of "Object.create" which has 'super' functionality, it provides ancestor method (or whole ancestor object) to methods that want to use it, I named it 'extend':sub = extend(main, {    extendedMethod: function (_super, arg1, arg2) {        _super(this, arg1, arg2);        // ...    },    newMethod: function (arg1, arg2) {         // ...    }});For the sake of readability I omitted descriptors in this example (I also use special functions to create them, as in plain version they're too verbose).In above case _super is extended method not whole ancestor object, I decided that way to make it less verbose, If I need access to ancestor object, then it's provided to '_parent' argument if one is given.This solution works very well if you work with Object.create

Mariusz Nowak said...

Sorry my post has been totally trimmed of formatting.
Is there any formatting guide for comments on this blog ? :)

Dana Carol said...

If the javascript has references to the HTML document object model (DOM), then you will have problems running it like it is a VBScript.

jimbojw said...

I don't understand how super can even be determined.  The statement "the prototype of the object in which the current method is located" doesn't make sense to me.

JavaScript doesn't have "methods" per se.  The term method implies a binding, and in JavaScript, there is none until when the function is executed, hence the confusion many developers have around "this".

So the idea of looking up the meaning of super becomes problematic.  What if I do something like var foo = Employee.prototype.describe; foo().  What should super be?  Should the JS engine know that foo's target (a function named 'me') happened to have been assigned at one time to a member named describe on an object called Employee.prototype?

Mariusz Nowak said...

jimbojw super is static, so it's determined at declaration point not at execution.
That way it would land in ES6 and that way it makes sense.

However if you wish you still can determine super dynamically (in ES5),
but it's not one line code:

proto.method = function self () {
   var _super = Object.getProtototypeOf(this)l;
   while (_super.method === self) {
      _super = Object.getPrototypeOf(_super);   }
   _super.method.call(this, ...);
};

Axel Rauschmayer said...

“The statement ‘the prototype of the object in which the current method is located’ doesn't make sense to me.” A method is currently executed. It is stored in an object. The search for a super-property starts in the prototype of that object. The diagram illustrates how this works – you need to get from Employee.prototype.describe() to Person.prototype.describe().

“JavaScript doesn't have ‘methods’ per se.” A property whose value is a function is called a method in JavaScript. Can you elaborate on what you mean by “hence the confusion many developers have around ‘this’”? The main problem I see in JavaScript is not how methods are handled but that (non-method) functions also have a value for `this`. If JavaScript methods worked like Python’s (the first explicit argument of a method is bound to the receiver of the method invocation), there wouldn’t be any problems.

“So the idea of looking up the meaning of super becomes problematic.” That is indeed an interesting problem. But note that what you are doing then is taking a class-like construct apart. But the pieces only make sense if they are used together. A method on its own hardly ever makes sense and even less so if it uses `super`.

Axel Rauschmayer said...

http://docs.disqus.com/help/19/

Axel Rauschmayer said...

Prototype.js [1] takes the same approach and I like it. You should be able to eliminate the `this` argument (by passing it to _super from the wrapper around extendedMethod). Caveat: Minification prevent this approach from working.

[1] http://www.2ality.com/2011/03/lightweight-javascript-inheritance-apis.html

Mariusz Nowak said...

Thanks, I didn't notice first that it's disqus engine

jimbojw said...

Thanks for taking the time to respond.  I'll try to clarify my confusion.

> " A method is currently executed. It is stored in an object. The search for a super-property starts in the prototype of that object."
I have a pretty good mental model of how "this" works in JS.  When you call a function using the method invocation syntax (ex: foo.bar()), then "this" inside the function points to whatever was on the left-hand side of the period prior to the invocation (ex: foo).

Now consider super.  When you call a function using method invocation syntax (ex: foo.bar()), then "super" inside the function points to ... "this"'s prototypal parent?  "this"'s prototypal parent's prototypal parent?

I believe it's the latter.  So if June was made from new Employee() (meaning Employee.prototype is June's prototypal parent), then we expect June.describe() to assign "super" to Person, right?  That would be June's prototypal grandparent.

What happens when you call a function directly via call() or apply()? (ex: foo.bar.call(null))? In that case, "this" is whatever is passed in explicitly, or the global object (window).  What would "super" be?

Under the "this's prototypal grandparent" line of reasoning, if "this" ends up being, say, the global window object, then "super" would be window's prototypal grandparent, which would be Object (maybe)?

> "A property whose value is a function is called a method in JavaScript. Can you elaborate on what you mean by “hence the confusion many developers have around ‘this’”? "

According to Wikipedia[1]:

"The association between class and method is called binding. A method associated with a class is said to be bound to the class. Methods can be bound to a class at compile time (static binding) or to an object at runtime (dynamic binding)."

This causes two problems in JavaScript: first, there are no classes, only prototypal inheritance, so the terminology gets confusing.  Second, JavaScript has no "compile time," there is only runtime.  Developers are routinely tripped up by "this" because they want to do the following:

someDomElement.onsomething = myObject.someMethod;

The reality is that when the JS engine executes someMethod, it does so outside the context of myObject, so "this" is not what the developer expected when they originally wrote the function or assigned it to a handler.

The quality of being a "method" in the OO sense is more than just being a property of an object which happens to be a function.  Being a method requires a binding to a class.  This binding doesn't exist until the function is executed, at which point the JavaScript engine decides what "this" ought to be.

My problem with "super" is that it's a concept steeped in the classical inheritance mindset and depends on ideas like classes, instances and methods---all of which are absent in varying degrees JavaScript.

[1] http://en.wikipedia.org/wiki/Method_(computer_programming)

jimbojw said...

I haven't been following ES past 5, and even that I'm not really up to speed on.  So I'll have to claim ignorance about future features.  If it's true that static is coming to ES6, I don't see how this differs from the failure that was ES4 (which I, for one, was happy to see die off).

Also, thanks much for your code example.  It seems to confirm my suspicion that the concept of "super" is a convienence variable for accessing the prototypal grandparent of "this".  I didn't know about getPrototypeOf, but I'm glad to see that the language is getting it---this was sorely needed.

Web Analytics