Prototypal Master Class, Flanagan
© 2011, Martin Rinehart
The current edition of David Flanagan's book, JavaScript: The Definitive Guide is the bestselling JavaScript book on Amazon as I write this (August, 2011). At the same time, the previous edition of "TDG" is the third bestselling JavaScript book on Amazon. (There is no shortage of JavaScript books on Amazon.)
Flanagan first published his Class()
method, which lets you create a class that has some of the best features of class-based classes, in a web article, Method chaining in JavaScript inheritance hierarchies dated July, 2008. We'll dive into it here. It may look impossibly long, especially by comparison to Crockford's prototypal methods, so we'll begin by cutting it down to size. And while we look at it, you'll see some JavaScript programming techniques that you'll want to make part of your own toolkits.
Class()
Is So Long!
Don't run away! Don't run for your magnifying glasses! This is just to give you an idea of the size of Flanagan's original Class()
:
/** * Class() -- 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. This function handles the repetitive * tasks of defining classes: setting up the prototype object for correct * inheritance, copying methods from other types, and so on. * * 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. * * methods: An object that specifies the instance methods for the class. * These functions are given an overrides property for chaining. * They can call "chain(this, arguments)" to invoke the method * they override. "arguments" must appear literally. * * 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. * * mixins: A constructor function or array of constructor functions. * The instance methods of each of the specified classes are copied * into the prototype object of this new class so that the * new class borrows the methods of each specified class. * Mixins are processed in the order they are specified, * so the methods of a mixin listed at the end of the array may * overwrite the methods of those specified earlier. Note that * methods specified in the methods object can override (and chain * to) mixed-in methods. * * This function is named with a capital letter and looks like a constructor * function. It can (but need not) be used with new: new Class({...}) **/ function Class(data) { // Extract the fields we'll use from the argument object. var extend = data.extend; var init = data.init; var classname = data.name || "Unnamed Class"; var statics = data.statics || {}; var mixins = data.mixins || []; var methods = data.methods || {}; // 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 Class() 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 for(var p in statics) constructor[p] = statics[p]; // Set superclass and classname properties of the constructor constructor.superclass = extend || Object; constructor.classname = classname; // Create an instance of the superclass to use as the prototype for // the new class. Assign it to constructor.prototype var proto = constructor.prototype = new constructor.superclass(); // Delete any local properties of the prototype object for(var p in proto) if (proto.hasOwnProperty(p)) delete proto[p]; // Borrow methods from mixin classes by copying methods to our prototype. if (!(mixins instanceof Array)) mixins = [mixins]; // Ensure an array for(var i = 0; i < mixins.length; i++) { // For each mixin class var m = mixins[i].prototype; // This is mixin prototype for(var p in m) { // For each property of mixin if (typeof m[p] == "function") // If it is a function proto[p] = m[p]; // Copy it to our prototype } } // Copy instance methods to the prototype object // This may override methods of the mixin classes or the superclass for(var p in methods) { // For each name in methods object var m = methods[p]; // This is the method to copy if (typeof m == "function") { // If it is a function m.overrides = proto[p]; // Remember anything it overrides proto[p] = m; // Then store in the prototype } } // All objects should know who their constructor was proto.constructor = constructor; // Finally, return the constructor function return constructor; } /** * This function is designed to be invoked with the this keyword as its * 1st argument and the arguments array as its 2nd: chain(this, arguments). * It uses the callee property to determine what function called this * function, and then looks for an overrides property on that function. * If it finds one, it invokes the overridden function on the object, * passing the arguments */ function chain(o, args) { m = args.callee; // The method that wants to chain om = m.overrides // The method it overrides if (om) return om.apply(o, args) // Invoke it, if it exists } // Example 9-11 demonstrates the methods above //----------------------------------------------- // A mixin class with a usefully generic equals() method for borrowing var GenericEquals = Class({ name: "GenericEquals", methods: { equals: function(that) { if (this == that) return true; var propsInThat = 0; for(var name in that) { propsInThat++; if (this[name] !== that[name]) return false; } // Now make sure that this object doesn't have additional props var propsInThis = 0; for(name in this) propsInThis++; // If this has additional properties then they are not equal if (propsInThis != propsInThat) return false; // The two objects appear to be equal. return true; } } }); // A very simple Rectangle class var Rectangle = Class({ name: "Rectangle", init: function(w,h) { this.width = w; this.height = h; }, methods: { area: function() { return this.width * this.height; }, compareTo: function(that) { return this.area() - that.area(); }, toString: function() { return "[" + this.width + "," + this.height + "]" } } }); // A subclass of Rectangle var PositionedRectangle = Class({ name: "PositionedRectangle", extend: Rectangle, init: function(w,h,x,y) { this.x = x; this.y = y; }, methods: { isInside: function(x,y) { return x > this.x && x < this.x+this.width && y > this.y && y < this.y+this.height; }, toString: function() { return chain(this,arguments) + "(" + this.x + "," + this.y + ")"; }, } }); var ColoredRectangle = new Class({ name: "ColoredRectangle", extend: PositionedRectangle, init: function(w,h,x,y,c) { this.c = c; }, methods: { toString: function() { return chain(this,arguments) + ": " + this.c} } }); var cr = new ColoredRectangle(1,2,3,4,5); alert(cr.toString()); // Demonstrate constructor and method chaining
That's almost 200 lines. By comparison, Crockford peaks at eight lines. But the comparison isn't apples-to-apples. Let's take another look at Flanagan's Class()
but this time we'll skip:
- How-to-use-it documentation
- How-to-use-it code samples
- How-it-works comments
Crockford had none of those in his code. (He had them all in the accompanying articles, just not in the code.) Here is Flanagan's code, extracted from the full listing.
function Class(data) { // Extract the fields we'll use from the argument object. var extend = data.extend; var init = data.init; var classname = data.name || "Unnamed Class"; var statics = data.statics || {}; var mixins = data.mixins || []; var methods = data.methods || {}; var constructor = function() { if (extend) extend.apply(this, arguments); // Initialize superclass if (init) init.apply(this, arguments); // Initialize ourself }; for(var p in statics) constructor[p] = statics[p]; constructor.superclass = extend || Object; constructor.classname = classname; var proto = constructor.prototype = new constructor.superclass(); for(var p in proto) if (proto.hasOwnProperty(p)) delete proto[p]; if (!(mixins instanceof Array)) mixins = [mixins]; // Ensure an array for(var i = 0; i < mixins.length; i++) { // For each mixin class var m = mixins[i].prototype; // This is mixin prototype for(var p in m) { // For each property of mixin if (typeof m[p] == "function") // If it is a function proto[p] = m[p]; // Copy it to our prototype } } for(var p in methods) { // For each name in methods object var m = methods[p]; // This is the method to copy if (typeof m == "function") { // If it is a function m.overrides = proto[p]; // Remember anything it overrides proto[p] = m; // Then store in the prototype } } proto.constructor = constructor; return constructor; } function chain(o, args) { m = args.callee; // The method that wants to chain om = m.overrides // The method it overrides if (om) return om.apply(o, args) // Invoke it, if it exists }
That listing shows the code we care about, plus (highlighted) the code related to "mixins", a topic we'll skip. We're just covering the green part.
Mixins—Not Today's Issue
A "mixin" is a bit of code that you can use in multiple, disparate classes. Although using mixins is a good technique, and Flanagan is doing us a favor by encouraging us to use them in our classes, "mixins" shed no light on inheritance hierarchies, so we'll skip that part of Flanagan's code. Now we're down to the green heart of the matter.
The General Idea
The Class()
method builds a constructor. You use it like this:
var SomeClassConstructor = Class( class_specs ); var my_instance = new SomeClassConstructor();
The Class Specification Object
The class_specs
parameter is a single object, a JavaScript technique that Crockford suggests, Flanagan uses and I use all the time. It makes sense when you have a lot of possible arguments and want a concise way of handling them. For example, I use a style_specs
argument that bundles any number of CSS styles into a single object. It looks a lot like the specs from a stylesheet:
var styles = { background: '#f0f0ff', border: '5px ridge #a0a0ff', position: 'absolute', . . . }
A big advantage of the style_specs
object is that you are already familiar with the CSS styles. You just need to remember that in JavaScript, you need to put quotes around the values and separate them with commas, not semicolons.
You call Flanagan's Class()
function with a single object that entirely specifies the class you want. The object may have these properties:
extend
: the constructor of the class being extendedinit
: a function that initializes the current classclassname
: the name of the current classstatics
: any class (not instance) propertiesmethods
: the instance methods
Flanagan also provides for mixins, but we're skipping them.
A big advantage of this technique, using a single object that holds all you need, is that you can use it to model anything:
var universe = { matter: [ /* one element per molicule */ ], big_bang: function () { /* model of the Big Bang */ }, . . . }
A disadvantage of this technique is that the programmer has to think carefully about what to model. Something a bit more restrictive than the entire universe would be good. Some of Flanagan's object properties are quite open-ended. For example, methods
is an array of functions. From his demonstration code (reformatted by your author):
. . ., methods: { area: function() { return this.width * this.height; }, compareTo: function(that) { return this.area() - that.area(); }, toString: function() { return "[" + this.width + "," + this.height + "]" } }, . . .
Let me draw your attention to the semi-conventional use of that
as a parameter name (in the compareTo()
function). This is popular with some when the this
object will be used with one other object, in a companion-like way:
function add( that ) { return this + that; }
The Class()
Function
Flanagan begins by initializing local variables, possibly improving readability and certainly making the typing easier.
Local Variables as Object Property References
The Class()
function begins by assigning the class_specs object's properties to local variables, sometimes assigning default values as it does so:
var clasname = data.name || "Unnamed Class";
You may recognize that idiom ( foo = property || default;
). It is a short and convenient way of saying:
if ( typeof property !== undefined ) { foo = property; } else { foo = default; } // or, more compactly foo = typeof property ? property : default;
I recommend coding so that the maintainer who needs to impove your code after you are gone will find it easy to read. (She had one semester of JavaScript three years ago.) Flanagan's idiomatic JavaScript fails that standard. On the other hand, he is writing here strictly for JavaScript programmers, an audience that should have no trouble with reading the code as he wrote it. You decide.
If I were asked for a peer review of this code I would object to the name data
for the class_specs object. It tells you nothing. class_specs
is imperfect but it's a step in the right direction.
Some of these simple assignments carry a surprisingly large payload. Keep your eye out for extend = data.extend;
.
The constructor
Function
Now, turn your brain up to Full. This two-liner is the heart of Class()
.
var constructor = function() { if (extend) extend.apply(this, arguments); // Initialize superclass if (init) init.apply(this, arguments); // Initialize ourself };
(I took the liberty of indenting in the standard way.) The Class()
function will end with the statement, return constructor;
. This is what it returns. Note that it does not execute these two statements. They will be executed when a constructor is created for your class. (Remember that "superclass" is the common, unfortunate jargon for the class that the current one extends.)
The apply()
method, along with its sibling call()
is rare. It's a method of every function. apply()
is assigned by the Function()
constructor. It executes the function to which it is attached, assigning its first argument to the this
variable. While call()
uses individual arguments, apply()
takes the arguments as an array. Note that constructor
is a function, a constructor to be precise, returned by Class()
. The this
and arguments
are being wrapped in a closure, one of the "magic" parts of JavaScript to which I object, but which I use if needed.
So the constructor function that Class()
returns will initialize the function that the current class extends, first, and then will initialize the current class. If you're like me, you'll want to fiddle with this bit until you are sure you understand it completely. This is the heart of the matter.
Class Statics
Next, class statics are copied to the constructor.
for(var p in statics) constructor[p] = statics[p];
If you read in my earlier series, Extending Instance Methods in JavaScript, you remember that I defined "class statics" at the end of the article and then pointed out that you could append them as properties of the constructor. Here we have either great minds thinking alike, or two competent coders doing a simple thing in a direct way.
The Rest of Class()
Finally the Class()
function winds down, taking care of details (like assigning its name) and taking care of the one thing that Crockford finds critical, but in a way that Crockford does not use. The prototype object is the extended class's constructor:
var proto = constructor.prototype = new constructor.superclass();
But the prototype's methods are not retained:
for(var p in proto) { if (proto.hasOwnProperty(p)) { delete proto[p]; } }
(Formatting by your author.) I leave you to check the remaining details, including the assignment to the proto.constructor
property. Does Flanagan agree with me about the "implicit" reference issue?
The chain()
Function
Flanagan uses an inelegant solution to the problem of finding methods from extended classes back up the inheritance chain. Frankly, so do I. We both solve the problem, but in entirely different ways. Why don't you make it a point to solve this problem for us? Something simple that a JavaScript beginner could read would be good.
Flanagan uses the callee
property of the arguments
pseudo array. This is official ECMA 262 standard JavaScript, supported in all major browsers. I refuse to use it. First, it is so obscure that the person who comes after to maintain your code will certainly not know what it means. Second, it may be so obscure that the TC 39 group decides to drop it in favor of something more direct. Look it up and use it at your own peril.
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
var A = Class({ name: "A", init: function( p0, p1 ) { this.a = p0; this.b = p1; }, methods: { toString: function() { return 'A{' + 'a=' + this.a + ',b=' + this.b + '}'; } } }); var B = Class({ name: "B", extend: A, init: function( p0, p1, p2, p3 ) { this.c = p2; this.d = p3; }, methods: { toString: function() { return 'B{' + chain(this,arguments) + ',c=' + this.c + ',d=' + this.d + ")"; }, } }); var C = new Class({ name: "C", extend: B, init: function( p0, p1, p2, p3, p4, p5 ) { this.e = p4; this.f = p5; }, methods: { toString: function() { return 'C{' + chain(this,arguments) + ',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
function Class(data) { // Extract the fields we'll use from the argument object. var extend = data.extend; var init = data.init; var classname = data.name || "Unnamed Class"; var statics = data.statics || {}; var methods = data.methods || {}; var constructor = function() { if (extend) extend.apply(this, arguments); // Initialize superclass if (init) init.apply(this, arguments); // Initialize ourself }; for(var p in statics) constructor[p] = statics[p]; constructor.superclass = extend || Object; constructor.classname = classname; var proto = constructor.prototype = new constructor.superclass(); for(var p in proto) if (proto.hasOwnProperty(p)) delete proto[p]; for(var p in methods) { // For each name in methods object var m = methods[p]; // This is the method to copy if (typeof m == "function") { // If it is a function m.overrides = proto[p]; // Remember anything it overrides proto[p] = m; // Then store in the prototype } } proto.constructor = constructor; return constructor; } function chain(o, args) { m = args.callee; // The method that wants to chain om = m.overrides // The method it overrides if (om) return om.apply(o, args) // Invoke it, if it exists }
Moving On
There are two more solutions examined in this series:
Resig's approach is short but not simple. Edwards' approach is neither. Both have considerable merit. If you think you have much to learn by studying the masters, these will reward you.
Feedback: MartinRinehart at gmail dot com
# # #