Method Chaining Part 2

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)}
    }
});

Books

Comprehensive coverage of Ruby 1.8 and 1.9

"The New Most Important Ruby Book"
Peter Cooper,
rubyinside.com

Completely updated for Ajax and Web 2.0

"A must-have reference"
Brendan Eich,
creator of JavaScript

The classic Java quick-reference