Prototypal Master Class, Edwards

© 2011, Martin Rinehart

Freelance JavaScripter Dean Edwards set the gold standard for inheritance in JavaScript when he released Base.js in 2006. (Since then he has continued to tweak his code. Today its copyright date is "2006-2010".) His original plus subsequent updates and lots of insightful discussion can be found on his blog at A Base Class for JavaScript Inheritance. Edwards is also well known for his Packer, a JavaScript minification program (for years one of the two standards at jscompress.com.

Sharp-eyed readers will have noted that Edwards' initial date is 2006, while our three others are all born in 2008. This is the first and still the most feature-rich of the crop.

You'll be glad to see that Edwards' code style (despite the very obscure check for _super he donated to Resig) is relatively straightforward once you understand a few idioms.

Overview of Base.js

The following is to give you an idea of the scope of the JavaScript in Base.js. As with Flanagan, do not run for the magnifiers—this is just to show the scope. Unlike the case with Flanagan, this is all code, so maybe a deep breath or a fresh mug of Joe would be good.

/*
	Base.js, version 1.1a
	Copyright 2006-2010, Dean Edwards
	License: http://www.opensource.org/licenses/mit-license.php
*/
    var Base = function() {
        // dummy
    };

    Base.extend = function( _instance, _static ) { // subclass
        var extend = Base.prototype.extend;

        // build the prototype
        Base._prototyping = true;
        var proto = new this;
        extend.call( proto, _instance );

        proto.base = function() {
            // call this method from any other method to
            // invoke that method's ancestor
        };

        delete Base._prototyping;

        // create the wrapper for the constructor function
        var constructor = proto.constructor;
        var klass = proto.constructor = function() {
            if ( !Base._prototyping ) {
                if ( this._constructing || this.constructor == klass ) {
                        // instantiation
                    this._constructing = true;
                    constructor.apply( this, arguments );
                    delete this._constructing;
                } else if (arguments[0] != null) { // casting
                    return ( arguments[0].extend ||
                             extend).call(arguments[0], proto );
                }
            }
        };

        // build the class interface
        klass.ancestor  = this;
        klass.extend    = this.extend;
        klass.forEach   = this.forEach;
        klass.implement = this.implement;
        klass.prototype = proto;
        klass.toString  = this.toString;
        klass.valueOf   = function( type ) {
            return ( type == "object" ) ? klass : constructor.valueOf();
        };
        extend.call( klass, _static );
        // class initialisation
        if ( typeof klass.init == "function" ) klass.init();
        return klass;
    };

    Base.prototype = {
        extend: function( source, value ) {
            if ( arguments.length > 1 ) { // extending with a name/value pair
                var ancestor = this[ source ];
                if ( ancestor &&
                    (typeof value == "function") && // overriding a method?
                    // the valueOf() comparison is to avoid circular references
                    (!ancestor.valueOf ||
                            ( ancestor.valueOf() != value.valueOf() )) &&
                    /\bbase\b/.test(value) ) {
                    // get the underlying method
                    var method = value.valueOf();
                    // override
                    value = function() {
                        var previous = this.base || Base.prototype.base;
                        this.base = ancestor;
                        var returnValue = method.apply( this, arguments );
                        this.base = previous;
                        return returnValue;
                    };
                    // point to the underlying method
                    value.valueOf = function( type ) {
                        return ( type == "object" ) ? value : method;
                    };
                    value.toString = Base.toString;
                }
                this[ source ] = value;
            } else if ( source ) { // extending with an object literal
                var extend = Base.prototype.extend;
                // if this object has a customised extend method then use it
                if ( !Base._prototyping && typeof this != "function" ) {
                    extend = this.extend || extend;
                }
                var proto = { toSource: null };
                // do the "toString" and other methods manually
                var hidden = [ "constructor", "toString", "valueOf" ];
                // if we are prototyping then include the constructor
                var i = Base._prototyping ? 0 : 1;
                while ( key = hidden[i++] ) { // WARNING: "="
                    if ( source[key] != proto[key] ) {
                        extend.call( this, key, source[key] );

                    }
                }
                // copy each of the source object's properties to this object
                for ( var key in source ) {
                    if ( !proto[key] ) extend.call( this, key, source[key] );
                }
            }
            return this;
        }
    };

// initialise
    Base = Base.extend(
        {
            constructor: function() {
                this.extend( arguments[0] );
            }
        },
        {
            ancestor: Object,
            version: "1.1",

            forEach: function( object, block, context ) {
                for ( var key in object ) {
                    if ( this.prototype[key] === undefined ) {
                        block.call( context, object[key], key, object );
                    }
                }
            },

            implement: function() {
                for ( var i = 0; i < arguments.length; i++ ) {
                    if ( typeof arguments[i] == "function" ) {
                        // if it's a function, call it
                        arguments[ i ]( this.prototype );
                    } else {
                        // add the interface using the extend method
                        this.prototype.extend( arguments[i] );
                    }
                }
                return this;
            },

            toString: function() {
                return String( this.valueOf() );
            }
        }
    );

Overall Structure

This is the overall structure of Base.js:

    var Base = function() {  /* dummy */  };

    Base.extend = function( _instance, _static ) {

        /* The "extend" instance method defined here. */

    };

    Base.prototype = {

        /* Base's prototype created here. */

    };

    Base = Base.extend(

        /* Base initializes itself here. */

    );

Base.js begins by defining an empty, dummy Base object on which it hangs Base.extend(). After defining Base.prototype, the extend() instance method is used to initialize the non-Dummy Base object.

The following example shows a traditional JavaScript version of the creation of an object instance using the same timing that Base.js uses:

  /* dummy object not needed */ 

function Foo( _intance, _static ) {  /* instance creation code */ }

Foo.prototype =  /* class creation code */ ;

var foo = new Foo();

Base.js follows that pattern, although you have to look closely because the extend() method is not a traditional constructor.

Also, Base.prototype will be the prototype method for every constructor that you create by extending Base. This means, among other things, that Base does not need to touch Object.prototype a practice that Edwards frowns on. (And so do I, even though Crockford adds to Object.prototype with his second—but only his second—iteration.)

We'll begin with a look at Base.extend, since it creates Base itself.

First, however, two issues starting with a comment on the use of key in this code. A general hash is a set of key/value pairs. In JavaScript, an object is not quite a general hash. It is a set of name/value pairs. (The keys, in hash terms, must be strings. They cannot be numbers, functions or other objects.) As you read the code, mentally substitute name for key as these all refer to object property names.

The second issue is what Edwards calls the "prototyping" phase. It may be necessary to create an empty object of a given class (person = new Person();) to transfer it's properties (including methods) to an extending class. This is problematic if the constructor requires time-consuming operations, such as requesting data from a server. Edwards solution is to have an init() method that the constructor calls. Time-consuming operations are placed in init(). Simple property creation is left in the constructor. Calling the constructor without "prototyping" gets the desired list of properties but does not call the init() method.

Now on to the code.

Base.extend

Base.extend() (or, more generally, X.extend()) returns a constructor for a new, extended class. It is called this way:

var X = Base.extend( /* args here */ );
. . .
var Y = X.extend( /* args here */ );

You call X.extend(), where X is Base or any constructor created by extending Base (or any constructor created by extending a constructor created by extending Base, and so on). The extend() method takes one or two arguments:

  1. The new instance properties of the class which you are creating, and
  2. (optionally) any class properties of the class you are creating.

To show this, let's look at Base.js being used to create our demo classes, A and B, where B extends A:

    var A = Base.extend(
        {
            constructor: function( p0, p1 ) {
                this.a = p0;
                this.b = p1;
            },
            toString: function () {
                return 'A{a=' + this.a + ',b=' + this.b + '}';
            }
        }
    );

    var B = A.extend(
        {
            constructor: function( p0, p1, p2, p3 ){
                this.base( p0, p1 );
                this.c = p2;
                this.d = p3;
            },
            toString: function () {
                return 'B{' + this.base() +
                        ',c=' + this.c + ',d=' + this.d + '}';
            }
        }
    );

The constructor property becomes the constructor function that is returned. The creation of B shows two uses of the base method. In the constructor, it passes parameters up the class hierarchy to the constructor of the class being extended (as Resig's _super does). Outside the constructor, it passes parameters up the class hierarchy to the method that the current method is overriding. In our example, it calls A's toString() method inside B's overriding toString() method.

The extend() is not hard to read if you are careful to read the timing precisely. For example, it is tempting to read the following as, "extend is set as a reference to Base.prototype.extend".

    var extend = Base.prototype.extend;

However, a more exact reading is, "When Base.extend() is called, extend will be set ... ". This is all in the future tense at this point. The method is being defined, not called.

Continuing, proto will be set to new this. (this will be X in X.extend( . . . ).) The extend.call( proto, _instance ) will assign all the properties in the object referred to by _instance as properties of the object referred to by proto.

After deleting the _prototyping flag, the var constructor will be assigned the proto.constructor property (if any). Then a variable klass will be created. ("class" is a JavaScript reserved word. If this were Resig we would probably have had "_class". I'm not sure which is better—or which is worse.) klass is the constructor being assembled.

The first job is to assign klass.constructor, a job made messy by the need to avoid calling long-running constructor processes. After finishing the constructor, the job gets simpler. See the discussion of the creation of Base below for notes on the individual properties.

After all these complications with the _instance object the handling of the the _static object is a one-liner: extend.call( klass, _static );. Recursion is great when it does lots of work with almost no code!

If any of the Base.extend() is not obvious yet (and it's not at all obvious on first reading) the discussion of the creation of Base (below) invites you to follow it through again with the actual _instance object literal that is used in creating Base. You can try it again, and this time read it in the present tense, as actual assignments are made.

Base.prototype

Base.prototype is the prototype of X in var X = Base.extend( . . . );. It is assigned a single property, extend(). It can be set up with two or one arguments, as its overall structure shows:

    Base.prototype = {
        extend: function( source, value ) {
            if ( arguments.length > 1 ) { // extending with a name/value pair

                 /* details omitted */ 

                this[ source ] = value;
            } else if ( source ) { // extending with an object literal

                 /* details omitted */ 

            }
            return this;
        }
    };

The this inside X.extend() is X, the constructor being extended. (X could be Base.) It is returned explicitly here as extend() is not called with the new operator.

The "name/value pair" Option

I'm going to pass on the "name/value pair" version of the arguments to Base.prototype.extend as it's another undocumented feature, but before leaving it, let's look at the long if test at the top.

This is the original:

if (ancestor && (typeof value == "function") && // overriding a method?
    // the valueOf() comparison is to avoid circular references
    (!ancestor.valueOf || ancestor.valueOf() != value.valueOf()) &&
    /\bbase\b/.test(value)) {

The first thing I did when reading this code was to reformat it so the logical and tests neatly lined up:

if (
        ancestor &&
        (typeof value == 'function') &&
        . . .
    ) {

(Actually, the first thing I did was to copy the code and open it in my text editor. That way you can do things like line up long tests.) Minor changes like this can make "foreign" code—code that you didn't write—a lot easier to decipher. If you always look at your own code as if it's foreign, you write better code.

The "object literal" Option

Here, extend() has been called with an object literal, as all of our examples show. There is only one part of this code that is not straightforward:

    // do the "toString" and other methods manually
    var hidden = ["constructor", "toString", "valueOf"];
    // if we are prototyping then include the constructor
    var i = Base._prototyping ? 0 : 1;
    while (key = hidden[i++]) {
        if (source[key] != proto[key]) {
            extend.call(this, key, source[key]);

        }
    }

Let's let Crockford's JSLint tool help us out. First, it will complain about the = operator used in the condition following while, and rightly so. That is almost always an error (it almost always should have been a logical == or ===). JSLint will also complain about the ++ operator, which is a regular source of errors. I'd suggest you go along with Crockford and eliminate it from your JavaScript.

Finally, there is one which JSLint misses. The while loop terminates, correctly, after processing the full array, as hidden[3] is undefined and while (undefined) is equivalent to while (false), which terminates the loop. Please don't write this way.

In this case, a standard for loop is much more readable:

    var i = Base._prototyping ? 0 : 1;
    for ( ; i < 3; i += 1 ) {
        key = hidden[i];
        . . .
    }

That still misses the essence, and in JavaScript you should look for the essence, as there's usually a way to get it. How about:

    if ( Base._prototyping ) {
        hidden = [ 'constructor', 'toString', 'valueOf' ];
    } else {
        hidden = [ 'toString', 'valueOf' ];
    }
    for ( name in hidden ) { . . . }

(Let's see, that's the _prototyping drop iota rho hidden take . . .. APL. "drop", "iota", "rho" and "take" are all single-character symbols. If you don't want readable, it's the only way to fly.)

Base Itself

The last section of the Base.js uses the extend() method to create the Base object. It calls extend this way:

// initialise
    Base = Base.extend(
        {
            /* extend's "_instance" param here. */
        },
        {
            /* extend's "_static" param here. */
        }
    );

Let's look at these two parameters in more detail.

The _instance Parameter

Understanding the first parameter means understanding that this is recursive. Loop back to the discussion above under the heading Base.extend using the following object as the value of the _instance parameter:

        {
            constructor: function() {
                this.extend( arguments[0] );
            }
        }

That defines the constructor for Base itself. When we write var A = Base.extend( a_instance_object, a_class_object ); we are assigning a_instance_object as arguments[0] in this constructor.

The _static Parameter

The second parameter is much longer, but is mostly straightforward. Its properties are:

Example

For each of the articles in this series we look at the code needed to create three objects, each having two properties of its own and a toString() method appropriate to all its properties. The second object extends the first; the third extends the second. Each extending object uses its extended object's toString() as part of its own toString() showing method overriding and access to overridden methods. Objects report the class of which they are instances, if applicable.

Application Code

This example is somewhat unfair. It uses just a small portion of the features of Base.js and it does not use the features that might show Base.js to best advantage. It is, however, exactly the same example we have used throughout this series. To change it would unfairly disadvantage the other solutions we have examined.

    var A = Base.extend(
        {
            constructor: function( p0, p1 ) {
                this.a = p0;
                this.b = p1;
            },
            toString: function () {
                return 'A{a=' + this.a + ',b=' + this.b + '}';
            }
        }
    );

    var B = A.extend(
        {
            constructor: function( p0, p1, p2, p3 ){
                this.base( p0, p1 );
                this.c = p2;
                this.d = p3;
            },
            toString: function () {
                return 'B{' + this.base() +
                        ',c=' + this.c + ',d=' + this.d + '}';
            }
        }
    );

    var C = B.extend(
        {
            constructor: function( p0, p1, p2, p3, p4, p5 ){
                this.base( p0, p1, p2, p3 );
                this.e = p4;
                this.f = p5;
            },
            toString: function () {
                return 'C{' + this.base() +
                        ',e=' + this.e + ',f=' + this.f + '}';
            }
        }
    );

    var o0 = new A( 0, 1 );
    var o1 = new B( 0, 1, 2, 3 );
    var o2 = new C( 0, 1, 2, 3, 4, 5 );

    alert( o0 ); // A{a=0, b=1}
    alert( o1 ); // B{A{a=0, b=1}, c=2, d=3}
    alert( o2 ); // C{B{A{a=0, b=1}, c=2, d=3}, e=4, f=5}

Library Code

/*
	Base.js, version 1.1a
	Copyright 2006-2010, Dean Edwards
	License: http://www.opensource.org/licenses/mit-license.php
*/
// alert( 'base.js loaded' );
    var Base = function() {
        // dummy
    };

    Base.extend = function( _instance, _static ) { // subclass
        var extend = Base.prototype.extend;

        // build the prototype
        Base._prototyping = true;
        var proto = new this;
        extend.call( proto, _instance );

        proto.base = function() {
            // call this method from any other method to
            // invoke that method's ancestor
        };

        delete Base._prototyping;

        // create the wrapper for the constructor function
        var constructor = proto.constructor;
        var klass = proto.constructor = function() {
            if ( !Base._prototyping ) {
                if ( this._constructing || this.constructor == klass ) {
                        // instantiation
                    this._constructing = true;
                    constructor.apply( this, arguments );
                    delete this._constructing;
                } else if (arguments[0] != null) { // casting
                    return ( arguments[0].extend ||
                             extend).call(arguments[0], proto );
                }
            }
        };

        // build the class interface
        klass.ancestor  = this;
        klass.extend    = this.extend;
        klass.forEach   = this.forEach;
        klass.implement = this.implement;
        klass.prototype = proto;
        klass.toString  = this.toString;
        klass.valueOf   = function( type ) {
            return ( type == "object" ) ? klass : constructor.valueOf();
        };
        extend.call( klass, _static );
        // class initialisation
        if ( typeof klass.init == "function" ) klass.init();
        return klass;
    };

    Base.prototype = {
        extend: function( source, value ) {
            if ( arguments.length > 1 ) { // extending with a name/value pair
                var ancestor = this[ source ];
                if ( ancestor &&
                    (typeof value == "function") && // overriding a method?
                    // the valueOf() comparison is to avoid circular references
                    (!ancestor.valueOf ||
                            ( ancestor.valueOf() != value.valueOf() )) &&
                    /\bbase\b/.test(value) ) {
                    // get the underlying method
                    var method = value.valueOf();
                    // override
                    value = function() {
                        var previous = this.base || Base.prototype.base;
                        this.base = ancestor;
                        var returnValue = method.apply( this, arguments );
                        this.base = previous;
                        return returnValue;
                    };
                    // point to the underlying method
                    value.valueOf = function( type ) {
                        return ( type == "object" ) ? value : method;
                    };
                    value.toString = Base.toString;
                }
                this[ source ] = value;
            } else if ( source ) { // extending with an object literal
                var extend = Base.prototype.extend;
                // if this object has a customised extend method then use it
                if ( !Base._prototyping && typeof this != "function" ) {
                    extend = this.extend || extend;
                }
                var proto = { toSource: null };
                // do the "toString" and other methods manually
                var hidden = [ "constructor", "toString", "valueOf" ];
                // if we are prototyping then include the constructor
                var i = Base._prototyping ? 0 : 1;
                while ( key = hidden[i++] ) { // WARNING: "="
                    if ( source[key] != proto[key] ) {
                        extend.call( this, key, source[key] );

                    }
                }
                // copy each of the source object's properties to this object
                for ( var key in source ) {
                    if ( !proto[key] ) extend.call( this, key, source[key] );
                }
            }
            return this;
        }
    };

// initialise
    Base = Base.extend(
        {
            constructor: function() {
                this.extend( arguments[0] );
            }
        },
        {
            ancestor: Object,
            version: "1.1",

            forEach: function( object, block, context ) {
                for ( var key in object ) {
                    if ( this.prototype[key] === undefined ) {
                        block.call( context, object[key], key, object );
                    }
                }
            },

            implement: function() {
                for ( var i = 0; i < arguments.length; i++ ) {
                    if ( typeof arguments[i] == "function" ) {
                        // if it's a function, call it
                        arguments[ i ]( this.prototype );
                    } else {
                        // add the interface using the extend method
                        this.prototype.extend( arguments[i] );
                    }
                }
                return this;
            },

            toString: function() {
                return String( this.valueOf() );
            }
        }
    );

Critique

Base.js is an awesome bit of JavaScripting, but I don't use it.

I parted company with Edwards when he described his very first goal: "I want to easily create classes without the MyClass.prototype cruft." Burying that "cruft" hides the essential nature of prototypal inheritance:

MyClass.prototype.toString = function () { . . . };

Spreading that out in a simple English sentence you get, "MyClass has a prototype object that includes a toString property that is a function that . . .". This is code that very precisely, very readably states what it does. It ain't broke. Don't fix it!


Feedback: MartinRinehart at gmail dot com

# # #