For newcomers to JavaScript, the language's prototypical inheritance model is often a source of confusion, particularly for those used to classical inheritance in languages like Java, C++, and Ruby. Right off the bat, new JS developers generally hit a few blocking questions:
- How do I set up inheritance without classes?
- What exactly does
new
even do? - What on earth is going on with the
this
keyword?
Once those are settled, the pieces begin to fall into place, and they can actually get started writing software. Eventually, though, another question arises: what about super
?
Invoking Methods from a Known Prototype
It turns out JavaScript's lack of super
keyword is due to the fact that the language's other features make it unnecessary. Since "methods" on an object are really just fields that happen to contain functions, the prototype model gives us everything we need to call super methods in JavaScript. All an object with an overriding method has to do is grab the function with the same name from its prototype and apply it:
That gets the job done, but it's a bit verbose, and it can be even worse in certain situations. For instance, at Salsify we use composite views from Marionette to simplify rendering individual item views for every model in a collection. But what happens when we're extending another composite view and want to override one of its item view's methods?
In addition to the verbosity of this approach, it has the downside of needing to know the name of the prototype when you're writing the method. This means that changes to your inheritance hierarchy can require updating every call site for a prototype method. It also prohibits reusing function implementations that need to invoke overridden functionality.
Invoking Methods from the Constructor's Prototype
To solve the problem of needing to know the prototype name (and put a cap on the verbosity), it might be tempting to use the object's constructor
attribute:
This works for the simple case; calling doSomething
on an instance of Child
behaves as expected. Unfortunately, calling it on an instance of Grandchild
doesn't go so smoothly. Since constructor
always refers to the actual function used to instantiate the object, the attempted super call instead becomes recursive, resulting in a stack overflow.
On the Shoulders of Giants
JavaScript has been around long enough (nearly two decades now) that plenty of different approaches have been taken to making it less tedious to invoke overridden methods. In 2008, John Resig published a solution on his blog that has seen considerable adoption. Lukas Olson has even packaged it as a robust Backbone plugin on GitHub. The end result of this approach is that it allows super invocations like this:
Short, sweet, and simple. The down side to this approach is that it ends up wrapping every function in a helper that maintains the value of the _super
attribute, updating it before and after each invocation. This means the stack is littered with calls through this wrapper during debugging. It can also have performance implications, because every method invocation (regardless of whether it's a super call or not) has to set up a try/finally block, which can be expensive in some JS engines.
The Prototype.js library attacks the problem from the outside in. To invoke a super method in a Prototype class, you declare your method to take a special $super
parameter as its first argument. The framework will pick this up and pass in the overridden implementation when the method is called.
Prototype's solution is consise and easy to read. It does, however, require some magic on the part of the framework to detect whether methods want the $super
parameter or not. It also bears considering that the value of that parameter is not the actual super implementation; it's wrapped in a helper that handles passing that method its $super
parameter if necessary, binding this
for the method invocation, etc. Overall, the Prototype approach ends up being fairly complex to implement.
Outside the JavaScript Box
Of course, the other option if a language doesn't have the feature you want is to switch to another language. Both CoffeeScript and TypeScript have super
built in, and compile it down to a static reference using the object's prototype.
CoffeeScript's super
looks similar to many of the JS solutions we've looked at:
TypeScript's, on the other hand, more closely resembles Java:
Our Approach
Here at Salsify, though, we're still operating in JavaScript, and we're experimenting with another approach. It has roughly the same form factor as John Resig's solution, but without requiring the wrapper function around every method. Instead, we define a dynamic super
property on the base prototype in our hierarchy that takes on the value of whatever method its caller overrides.
Using it is as simple as:
One key difference between this and the earlier solutions is that this.super
returns the actual implementation defined on the parent, without any wrappers or bound state being pulled along. There's no performance overhead when you call it or extra frames to wade through in the debugger; it's just a vanilla JavaScript function.
There are two things you may notice about this implementation. First, the use of Object.defineProperty
means it won't work in IE 8 or older. Second, it uses the dubious Function.caller
, which will no longer officially be available in the next version of JavaScript, generally referred to as ES6 or "Harmony". Fortunately, ES6 will also introduce language support for accessing super methods, so the moment that becomes a problem will also be the moment we won't need this shim any longer.
Until then we'd love to hear your take on any of the approaches here, or others you've tried out.