RR

Refining Superclass Method Calls in JavaScript

February 10, 2007

Originally posted to ydnar.vox.com in February 2007.

Last week I was revisiting the always fun problem of implementing “classical” inheritance in JavaScript. I’d taken a few stabs at it, and had gotten it to a reasonably good state that borrowed some good ideas from Doug Crockford, Sam Stephenson, and Dean Edwards. Joshua Gertzen wrote a good post about various methods on his blog.

I’ve never been terribly thrilled with the form Class.superClass.method.apply( this, arguments ). It was redundant: replicating both the class and method names. Copy & paste of code could lead to subtle errors, and it’s annoying to type that much. But the alternatives were worse: Recompiling the function to generate a “magic” lexical for the superclass or wrapper methods. So the Class object basically sat untouched for a year and a half.

Back to last week…It occurred to me that in all the JavaScript we’d built for Vox, we almost never shared a method between two objects, except via inheritance. There were a couple exceptions, but they could be rewritten (it turned out to be a good idea anyway). Second, functions are objects like everything else, and can have arbitrary properties. Third, arguments.callee is available in every function call in JavaScript. I realized then that storing the superclass was not as useful as just storing the supermethod.

For any given method in a class, store its supermethod as a property of the method: method.__super. Instead of the unwieldy construct above, any method could simply use arguments.callee.__super.apply( this, arguments ).

The Class constructor from Core.js:

Class = function( sc ) {
var c = function( s ) {
this.constructor = arguments.callee;
if( s === __SUBCLASS__ )
return;
this.init.apply( this, arguments );
};

c.override( Class );
sc = sc || Object;
c.override( sc );
c.__super = sc;
c.superClass = sc.prototype;

c.prototype = sc === Object ? new sc() : new sc( __SUBCLASS__ );
c.prototype.extend( Class.prototype );
var a = arguments;
for( var i = 1; i < a.length; i++ )
c.prototype.override( a[ i ] );

for( var p in c.prototype ) {
var m = c.prototype[ p ];
if( typeof m != "function" || defined( m.__super ) )
continue;
m.__super = null;
var pr = sc.prototype;
while( pr ) {
if( defined( pr[ p ] ) ) {
m.__super = pr[ p ];
break;
}
if( pr === pr.constructor.prototype )
break;
pr = pr.constructor.prototype;
}
}

return c;
}

arguments.callee was useful in the constructor too: Instead of creating a circular reference by overriding the constructor like this: constructor.prototype.constructor = constructor, the constructor itself can just set it on the this object when the constructor is called: this.constructor = arguments.callee.

Calling a supermethod can be simplified further, to arguments.callee.applySuper( this, arguments ) via a little sugar:

Function.prototype.extend( {
applySuper: function( o, args ) {
return this.__super.apply( o, args );
},

callSuper: function( o ) {
var args = [];
for( var i = 1; i < arguments.length; i++ )
args.push( arguments[ i ] );
return this.__super.apply( o, args );
}
} );