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