The comments on my last post about method chaining in JavaScript were spectacular, and I want to publicly thank all who took the time to read my code and think about it. The final version of the code (which you can see below the fold) is much stronger thanks to their comments.
Here's the new version of the defineClass() function (along with helper functions heir() and chain()). It will replace Example 9-10 in my book:
/**
* defineClass() -- a utility function for defining JavaScript classes.
*
* This function expects a single object as its only argument. It defines
* a new JavaScript class based on the data in that object and returns the
* constructor function of the new class.
*
* The object passed as an argument should have some or all of the
* following properties:
*
* name: the name of the class being defined.
* If specified, this value will be stored in the classname
* property of the returned constructor object.
*
* extend: The constructor of the class to be extended. The returned
* constructor automatically chains to this function. This value
* is stored in the superclass property of the constructor object.
*
* init: The initialization function for the class. If defined, the
* constructor will pass all of its arguments to this function.
* The constructor also automatically invokes the superclass
* constructor with the same arguments, so this function must expect
* the same arguments, in the same order, as the superclass
* constructor, and can add additional arguments at the end.
*
* methods: An object that specifies the instance methods (and other
* non-method properties for the class. The properties of
* this object become properties of the prototype. Methods
* are given an overrides property for chaining. They can
* call "chain(this, arguments)" to invoke the method they
* override. This function adds properties to the methods in
* this object, so you may not pass the same method in two
* invocations of defineClass().
*
* statics: An object that specifies the static methods (and other static
* properties) for the class. The properties of this object become
* properties of the constructor function.
**/
function defineClass(data) {
// Extract some properties from the argument object
var extend = data.extend;
var superclass = extend || Object;
var init = data.init;
var classname = data.name || "Unnamed class";
var methods = data.methods || {};
var statics = data.statics || {};
// Make a constructor function that chains to the superclass constructor
// and then calls the initialization method of this class.
// This will become the return value of this defineClass() method.
var constructor = function() {
if (extend) extend.apply(this, arguments); // Initialize superclass
if (init) init.apply(this, arguments); // Initialize ourself
};
// Copy static properties to the constructor function
if (data.statics)
for(var p in data.statics) constructor[p] = data.statics[p];
// Set superclass and classname properties of the constructor
constructor.superclass = superclass;
constructor.classname = classname;
// Create the object that will be the prototype for the class.
// This new object must inherit from the superclass prototype.
var proto = (superclass == Object) ? {} : heir(superclass.prototype);
// Copy instance methods (and other properties) to the prototype object.
for(var p in methods) { // For each name in methods object
if (p == "toString") continue; // Handled below
var m = methods[p]; // This is the value to copy
if (typeof m == "function") { // If it is a function
m.overrides = proto[p]; // Remember anything it overrides
m.name = p; // Tell it what its name is
m.owner = constructor; // Tell it what class owns it.
}
proto[p] = m; // Then store in the prototype
}
// In IE, a for/in loop won't enumerate properties that have the same name
// as non-enumerable Object methods like toString(). As a partial
// work-around, we handle the toString method specially
if (methods.hasOwnProperty("toString")) { // IE DontEnum bug
methods.toString.overrides = proto.toString;
methods.toString.name = "toString";
methods.toString.owner = constructor;
proto.toString = methods.toString;
}
// All objects should know who their constructor was
proto.constructor = constructor;
// And the constructor must know what its prototype is
constructor.prototype = proto;
// Finally, return the constructor function
return constructor;
}
/**
* Return a new object with p as its prototype
*/
function heir(p) {
function h(){}
h.prototype=p;
return new h();
}
/**
* Chain from the calling function to the function on its overrides property.
* Invoke that method on the first argument. The second argument must be the
* arguments object of the calling function: its callee property is used to
* determine what function is doing the chaining. The third argument is an
* optional array of values to pass to the overridden method. If omitted,
* the second argument is used instead, passing all of the caller's arguments
* on to the overridden method.
*
* This method returns the return value of the overridden method or
* throws "ChainError" if no overridden method could be found
*
* Typical invocation: chain(this, arguments)
* To pass different args: chain(this, arguments, [w, h])
*/
function chain(o, args, pass) {
var f = args.callee; // The calling function.
var g = f.overrides; // The function it chains to.
var a = pass || args; // The arguments we'll pass to s
if (g) return g.apply(o, a); // Call o.g(a) and return its value as ours.
else throw "ChainError" // Complain if nothing to override
}
And here is code that uses defineClass(). It will replace Example 9-11.
// A very simple Rectangle class
var Rectangle = defineClass({
name: "Rectangle",
init: function(w,h) {
this.w = w;
this.h = h;
},
methods: {
area: function() { return this.w * this.h; },
toString: function() { return "[" + this.w + "," + this.h + "]" }
}
});
// A subclass of Rectangle
var PositionedRectangle = defineClass({
name: "PositionedRectangle",
extend: Rectangle,
init: function(w,h,x,y) {
// Automatic chain here: Rectangle.call(this,w,h,x,y)
this.x = x;
this.y = y;
},
methods: {
isInside: function(x,y) {
return x > this.x && x < this.x + this.w &&
y > this.y && y < this.y + this.h;
},
toString: function() {
return chain(this, arguments) + "(" + this.x + "," + this.y + ")";
}
}
});
var ColoredRectangle = defineClass({
name: "ColoredRectangle",
extend: PositionedRectangle,
init: function(w,h,x,y,c) { this.c = c; },
methods: {
toString: function() { return this.c + ": " + chain(this,arguments)}
}
});



