Refining Superclass Method Calls in JavaScript
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 );
}
} );
Comments
Type.registerNamespace("Demo");
Demo.Person = function(firstName, lastName, emailAddress) {
this._firstName = firstName;
this._lastName = lastName;
this._emailAddress = emailAddress;
}
Demo.Person.prototype = {
getFirstName: function() {
return this._firstName;
},
getLastName: function() {
return this._lastName;
},
getName: function() {
return this._firstName + ' ' + this._lastName;
},
dispose: function() {
alert('bye ' + this.getName());
}
}
Demo.Person.registerClass('Demo.Person', null, Sys.IDisposable);You can extend the class via inheritance, like creating an employee.
Demo.Employee = function(firstName, lastName, emailAddress, team, title) {
Demo.Employee.initializeBase(this, [firstName, lastName, emailAddress]);
this._team = team;
this._title = title;
}
Demo.Employee.prototype = {
getTeam: function() {
return this._team;
},
setTeam: function(team) {
this._team = team;
},
getTitle: function() {
return this._title;
},
setTitle: function(title) {
this._title = title;
},
toString: function() {
return Demo.Employee.callBaseMethod(this, 'toString') + '\r\n' + this.getTitle() + '\r\n' + this.getTeam();
}
}
Demo.Employee.registerClass('Demo.Employee', Demo.Person);