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:
- The new instance properties of the class which you are creating, and
- (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:
ancestor
—TheObject
constructor.version
—String literal "1.1". (Note that the version in the initial comments is "1.1a". Beware of duplicating data in comment and code. It will inevitably get out of synch.)forEach
—A method for looping through object properties, preferred by some to JavaScript's built-infor/in
loop.implement
—See blog comments starting at #67. An otherwise undocumented method that Edwards provides for using interfaces in lieu of multiple inheritance. (The problem with multiple inheritance:B1
andB2
extendA
.C
extendsB1
andB2
.B1
andB2
both override a method inA
. For that method, what doesC
use?)toString
—A simple method returning thevalueOf
the object. For objects descended fromBase
this will be the name of the object's constructor.
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
# # #