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].
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)'
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:
Person.prototype.describe.call(this)All the steps are performed:
Person.prototype
Person.prototype.describeNote that we also find a describe if there isn’t one in Person.prototype directly, but in one of its prototypes.
Person.prototype.describe.call(this)
Employee.prototype.constructor === EmployeeThat 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.
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]]:
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 definedThere 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; };