Calling Superclass Methods in JavaScript colorful lead photo

Overriding a method and invoking the same method on the superclass is core technique in object-oriented programming. It's great: you automatically preserve the behavior of the superclass now and in the future. In JavaScript, the most straight forward way is to refer to the superclass's method directly and use apply() to pass it the current scope and arguments:

function Wagon() {}
subclass(Wagon, Vehical, {
    drive: function() {
        this.moveForward(3);
    }
});

function RickityWagon() {}
subclass(RickityWagon, Wagon, {
    drive: function() {
        this.clatter();
        return Wagon.prototype.drive.apply(this, arguments);
    }
});

No matter how Wagon's drive() method evolves - adding more complex logic, calling other methods, adding option arguments, returning a value - RickityWagon's drive() method will always behave the same way, just with additional clattering.

It may seem strange to refer to the superclass directly if you come to Java, but that's perfectly natural for those of us coming from C++. You see, in C++ you can have multiple base classes, so super.method is ambiguous, so you have to say which class you mean.

So that's superclass chaining. Pretty much everyone agrees that it's a good thing. Some people, however, think the obvious way, return SuperClass.prototype.method.apply(this, arguments), is too verbose and go looking for an easier way. I'd like to discuss of few of those approaches and why they don't quite work, and argue for sticking to the simple, explicit, and obvious way.

The first thing you'd think of is to put a super property on the class's prototype, so you could say something like this.super.drive. Unfortunately, this breaks down for more than one level of inheritance. If you call up to a superclass, and it tries to call up to it's superclass, it will end up calling back into itself, causing an infinite loop.

Resig's _super()

John Resig attacks this problem directly in his Class.extend utility:

prototype[name] = typeof prop[name] == "function" && 
    typeof _super[name] == "function" && fnTest.test(prop[name]) ?
    (function(name, fn){
      return function() {
        var tmp = this._super;

        // Add a new ._super() method that is the same method
        // but on the super-class
        this._super = _super[name];

        // The method only need to be bound temporarily, so we
        // remove it when we're done executing
        var ret = fn.apply(this, arguments);        
        this._super = tmp;

        return ret;
      };
    })(name, prop[name]) :
    prop[name];

His approach is very direct: when you call _super, swap out _super on the parent temporarily, and then put it back when you're done. Which is great, unless fn throws an exception, in which case it doesn't get put back. You could put a try/catch/rethrow around it, but that usually drops important stack trace information from the exception.

Using caller

There's another interesting way to implement super: relying on arguments.callee.caller. Using that, you could implemnt a superApply method that you call like so:

function RickityWagon() {}
subclass(RickityWagon, Wagon, {
    drive: function() {
        this.clatter();
        return this.superApply(arguments);
    }
});

superApply could use arguments.callee.caller to get a reference to the method it's inside, which you could link back to the overridden method on the superclass if you set up a link when creating the class. Let's say you detect overridden methods at class creation at set a parentMethod property on the overriding method. The superApply could be implemented as:

function superApply(args) {
    var pm = arguments.callee.caller.parentMethod;
    if ( pm ) return pm.apply(this, args);
}

There are a couple of problems with that. First of all, caller is deprecated: you really shouldn't be using it at all, and especially not for some merely convenient syntactic sugar. If an environment drops support for it altogether (and many JavaScript engines already provide a faster "strict" mode where callee is not supported) you'll have to change a lot of code to get it working again.

Second, this relies on behind-the-scenes stuff happening correctly at class creation. It pretty much ties you to one class creation mechanism. If anyone tries to modify the class on after the fact, they'll have to know about those magic properties.

Just Too Complex

Both those approaches are just too complex. They're hard to understand, hard to explain, and buggy in practice. They are, however the best solutions that top minds have come up with for implementing super starting from this. I certainly wasn't able to come up with anything nearly as good. As a result, I seriously doubt there is a clean way to make this work. My conclusion would be simply "don't do that."

A guy goes to a doctor and says, "every time I lift my arm like this, I get a sharp pain." So the doctor says, "don't do that."

Hanging Super on the Class

So, super behavior has to hang off the class. We're all in agreement on that, right? The only remaining question is if we should reference the superclass directly, or go through our own class. If we just hard-code in the name of the superclass, it looks like this:

return Wagon.prototype.drive.apply(this, arguments);

If on the other hand, we keep a reference to the superclass's prototype on the base class, say, _super, then we can write this:

return RickityWagon._super.drive.apply(this, arguments);

Not much difference, really, but some people prefer this so that it's easy to change the class later to have another base class. That's not a particularly common refactoring, in my experience, but it doesn't sound like a bad idea to configure the superclass in one place.

So what's not wrong with this, I ask? It's not unclear, is it? It's very, very clear. If you understand prototype, and you understand apply, this, and arguments... it's just so painfully obvious and clear. Is it so ugly, then? To me, the baroque alternatives are a thousand times uglier. Perhaps we should simply meditate on the lessons of this expression instead of trying to replace it with something barely more concise but opaque and fraught with hidden complexity. And if a beginner asks you what it means, perhaps you could encourage them to come to a deeper understanding of JavaScript by internalizing these fundamental concepts.

- Oran Looney May 24th 2011