This is part 2 of the subject. For a primer on prototype-based inheritance in Javascript, see the previous article.
Javascript is a prototype-based language, but sometimes it pretends not to be. Constructors, types, the new and instanceof operators -- these are all an additional layer built on top of the core model. And they make it a little difficult to tell what is really going on... So we're going to take them out of the picture. Here's a summary of the last article in SpiderMonkey Javascript.
// Prototype inheritance
A = { foo: true }; // new A
B = {}; // new B
B.__proto__ = A; // If a property is not found in B, look in A
alert(B.foo); // true
B.foo = false;
alert(A.foo); // still true
// the 'new' operator can be reproduced in SpiderMonkey Javascript
function nuevo (constructor) {
var newObject = {}; // new object with its own properties
newObject.constructor = constructor; // this is the new object 'type'
newObject.__proto__ = constructor.prototype; // desired prototype obtained here
// call the constructor on the newly created object
constructor.apply(newObject, Array.prototype.slice.call(arguments, 1));
return newObject;
}
Ok, so now that you know how prototype inheritance works, lets make it a little easier to use. There are already many good libraries that make "classical" or class-based inheritance easier in Javascript. (My favorite is the simple one provided by John Resig on his blog.) But I'm not yet aware (I'm sure you'll help me here) of any that make prototype-based programming simpler. Douglas Crockford, Javascript god, has a simple function posted on his site. We'll start with that.
Object.create = function (o) {
function F() {}
F.prototype = o;
return new F();
};
You should know by now what this is doing. The constructor function F is necessary because there is no universal way to set a prototype chain directly, other than specifying it on a constructor. The type F is a throwaway. You don't need it anymore. You won't be using the new operator, and the instanceof operator makes no sense for pure prototype-based programming anyway. (Instead, you would use the Object.prototype.isPrototypeOf method found on every object.)
Using Crockford's function, our first code example now looks like this.
// Prototype inheritance
A = { foo: true }; // new A
B = Object.create(A); // new B
Pretty simple, and it's almost all you need. But I find that in practice, some additional features are useful. Here's a first iteration of my version.
var Proto = {
__proto__: Object.prototype,
clone: function(init) {
var Clone = function(init) {
this.__proto__ = Clone.prototype;
this.constructor = Clone;
if (init) this.assign(init);
}
Clone.prototype = this;
return new Clone(init);
},
assign: function(props) {
for (var key in props) {
var value = props[key];
if (this[key] != value)
this[key] = value;
}
return this;
}
}
There's not a whole lot of difference between my own and Crockford's here besides naming, but there are some significant things going on. For one, Crockford attaches his function as a property of the Object constructor. This makes perfect sense, unless you consider that the whole reason he scrapped his first iteration (in which the function was simply named object) was that "globals are clearly problematic". But you actually have all the same problems (namespace issues, etc) here.
On another of his iterations, he attached the method begetObject to the Object.prototype. The problem here is that adding anything to Object.prototype will cause it to be inherited by every single object in the global namespace, whether or not it's actually part of your program. I made a compromise and added my clone method to the prototype of the Proto object. So only objects which inherit from Proto will be affected. Of course, there are still no guarantees that other code in the global namespace will not interact with the name Proto, but you can name this whatever you will (eg. biz$techhead$Proto if you like).
Another thing you'll notice about my "improvements" is that I explicitly set the __proto__ property of the new object. The __proto__ property really only means anything in Mozilla's Javascript, and I simply set it to what it was already. The reason I set it is because it is a useful relationship to preserve, and now I have read access to it even in Internet Explorer. I also set the constructor property for the sake of correctness.
Lastly, you'll notice that I added an optional initializer to assign properties to the newly constructed object. This is merely a shortcut.
But I'm not quite done. For one, it is useful on occasion, even when using prototypes, to be able to call a method on a super prototype. A naive attempt and its correction is shown below.
A a = Proto.clone({
create: function(name) { return this.clone({ name: name }) }
});
B b = A.clone({
create: function(name, gender) {
// this may look right at first glance
// but will quite often end up in an endless loop
// see if you can figure out why
// var o = this.__proto__.create.call(this, name);
// instead, this is correct
var o = A.create.call(this, name);
o.gender = gender;
return o;
}
});
But I don't really like having to refer to the super prototype by name. If I decide to change the name from A to something else, I've got to change B also. Not a disaster but it is still annoying. So I provided a way to access the super prototype through a generic name.
A bigger issue I've found is that sometimes a prototype needs to be able to control exactly how it is "copied". Sure, it could override the clone method each time, but that is a lot of extra work for a common task, so I set up an event instead. Behold the next iteration.
var Proto = {
__proto__: Object.prototype,
clone: function(init) {
var Clone = function(init) {
this.__proto__ = Clone.prototype;
this.constructor = Clone;
this.onclone();
if (init) init.call(this, this.__proto__);
}
Clone.prototype = this;
return new Clone(init);
},
onclone: function() {}
}
Notice that on my last iteration, you could provide an object to the clone method and it would copy its properties onto the new object. In this iteration, you may provide an initialization function (much like a constructor) to set up the new object. This provides a couple of new advantages. See the example below.
Animal = Proto.clone(function() {
this.eat = function(food) {}
this.sleep = function() {}
this.poop = function() {}
this.breed = function(animal) {}
});
Human = Animal.clone(function(_super) {
var ate = []; // now I get a new private scope
this.eat = function(betterfood) {
_super.eat.call(this, betterfood); // can use _super instead of Animal
// do something else
ate.push(betterfood);
// sorry about the lame example
}
});
So, with the latest and greatest iteration, I get access to a _super keyword and the ability to easily add private members to an object. But wait! The private member ate is now shared by all humankind. This is probably not what the programmer had in mind. Instead, this probably was...
Human = Animal.clone(function(_super) {
var init = function() {
var ate = []; // now I get a new private scope
this.eat = function(betterfood) {
_super.eat.call(this, betterfood);
ate.push(betterfood);
};
};
this.onclone = function() {
_super.onclone.call(this);
init();
};
init();
});
Now each Human keeps his own menu to himself. But that's a lot of extra typing for something that should be simple, so I'll add one last bit of magic...
/**
* The prototype of all objects in this small
* prototype framework.
*/
var biz$techhead$Proto = {
__proto__: Object.prototype,
/**
* Clone this object and (optionally) invoke an initializer.
* Should NOT be overriden.
* Override 'onclone' instead.
*/
clone: function(init) {
var Clone = function(init) {
this.__proto__ = Clone.prototype;
this.constructor = Clone;
this.onclone();
if (init) {
if (init instanceof Function)
this.init(init);
else
this.assign(init);
}
}
Clone.prototype = this;
return new Clone(init);
},
/**
* This event fires on the new child of this object
* whenever 'clone' is called.
* It is analagous to a constructor.
*/
onclone: function() {},
/**
* Invokes an initializer function on this object.
* It supplies this object's super prototype as the
* first argument, and provides an optional second
* argument to those initializers that declare
* two or more parameters. The second argument is a
* function that will install the initializer 'onclone'
* when invoked.
*/
init: function(init) {
var _this = this,
_super = this.__proto__;
if (init.length > 1) {
init.call(this, _super,
function() {
var hasonclone = _this.hasOwnProperty('onclone'),
onclone = _this.onclone;
_this.onclone = function() {
var _this = this;
var f = hasonclone ?
function() { onclone.call(_this) } :
function() { _super.onclone.call(_this) };
init.call(this, _super, f);
};
});
} else {
init.call(this, _super);
}
},
/**
* Shallow copies the given properties onto this object,
* excluding those already owned by this object
* (eg. those inherited through Object.prototype).
*/
assign: function(props) {
for (var key in props) {
var value = props[key];
if (this[key] != value)
this[key] = value;
}
return this;
}
}
I just added a bit of sugar that allows you to set the 'onclone' event handler to the initializer in one step. Now the following is possible.
Human = Animal.clone(function(_super, install) {
install(); // This initializer method will run 'onclone' also.
// install() will automagically be replaced with a call
// to _super.onclone when called from the 'onclone' event
var ate = [];
this.eat = function(betterfood) {
_super.eat.call(this, betterfood);
ate.push(betterfood);
}
});
You can find the "finished" framework here. I hope you enjoyed this journey as much as I did. Please comment, and let me know your thoughts on prototype-based program design.
If you like to read about an earlier exploration of prototype based programming in JavaScript that results in a similar if somewhat simpler approach to things, please see:
http://blog.blainebuxton.net/2008/08/update-on-javascript.html
RPW
April 4, 2009 8:28 PM