Saturday, November 20, 2010

A case for functional inheritance

Having written quite a fair amount of JavaScript over the years, I'd like to pretend that I've at least reached some intermediate level of skill in the language. One of the fascinating things about JavaScript is the great deal of flexibility afforded in how a particular task can be performed - and one of the tasks with the greatest variance would be implementing objects/classes.

The conventional advice is to learn to love prototype inheritance - which basically means understanding the magical dance between functions, their prototype field, and the various gotchas around the 'this' field.

While prototype inheritance can work, it's subject to a couple of pieces of weirdness - certainly enough to make it harder than it needs be.

Firstly, JS functions are press-ganged into 3 roles:
* regular functions
* object functions
* constructor functions

What makes it more exciting is that the role is defined by how the function is invoked, not by how it is defined, which can often have interesting consequences.
function myConstructor() {
this.x = 12;
}
Writing "new myConstructor()" would have the intended consequence of constructing a new object with a field called 'x', forgetting the 'new' and writing "myConstructor()" would magically define a new global variable x. As I said: exciting.

Object methods suffer from a similar problem, if we presume something like:
myConstructor.prototype.setValue = function(v) {
this._value = v;
}
then while
var myObj = new myConstructor();
myObj.setValue(5);
would behave properly, separating the method from the object yields hilarity.
var myObj = new myConstructor();
var fn = myObj.setValue;
fn(5);
Similarly as for leaving out 'new' with the constructor, this code will helpfully create a new global variable _value.

While that last example might appear to be a bit contrived, it's actually the same process as happens whenever a supposed object method is bound as a function callback - such as to mechanisms such as the setTimeout method or the comparison callback for sorting arrays.

So something like "setTimeout(myObj.doDeferredThang, 1000)" would call the function bound to the doDeferredThang slot, but would cheerfully have 'this' pointing to the wrong thing - typically the window object.

Experienced JavaScripters would use something like bind or a dedicated trampoline, but this requires vigilance to remember all the places that prototype inheritance will screw you over for a nickel.

The fundamental problem with prototype inheritance is that it's a clever feature for clever programmers. Make one slip however and weird shit hits the fan.

The presence of the mechanisms for prototype inheritance provides a natural gravity towards trying to cooperate and/or paper over the problems (e.g. see aforementioned bind mechanism) - but my personal conclusion is that the whole approach is not salvageable.

One of the core features/problems with prototype inheritance is that object methods aren't first class concepts - you can't distribute the method from an object without also distributing the object. To put it into context, it's barely one step outside of the Kingdom of Nouns.

The alternate approach is to make the methods bounds to the object, regardless of whether they are detached or not.
function makeMyObject() {
var self = {};

self.setValue = function(v) {
self._value = v;
}

self.doDeferredThang = function() {
...blah...
}

return self;
}
Compared to the prototype inheritance, this is (at least in my opinion) a lot more straightforward.
  • Functions are just functions again. There is no special case behavior for constructor functions or object functions.
  • The closure means that methods remember the object that they are bound to. So all of the examples above where prototype inheritance made things exciting would be boringly predictable in this form. Also note that "this" will still be available if necessary - so feel free to add a prefix along the lines of "if (this !== self) alert('I would have been so boned here');"
This form also enables some other capabilities that aren't possible in the prototype model. For example, there's no reason that "_value" needs to be stored against the object - it could just as easily be completely private in a local variable within the closure.

If you need a class hierarchy, it is easily handled by replacing the "var self = {};" with something like "var self = makeMySuperclass(...mysuperargs...);".

Not everything is butterflies and Barney however, there are a number of disadvantages with functional inheritance:
  • Objects will use up more memory and take longer to initialize. These overheads aren't terribly large, but could add up if you had zillions of objects. In this case, you could either model the classes that covered the zillions of objects with prototype inheritance, or - heaven forbid - model them as zillions of data instances with a singleton controller.
  • Doing things like sticking private date into variables within the closure requires you define all of the methods that directly access that state within the closure. That being said, people may think that making something private actually being enforced is a good idea.
  • The factory methods have complete control over what is present in a stock object. e.g. in prototype inheritance you can drunkenly add methods to a class anywhere you like. For functional inheritance, it's "Ecto Gamut!" - at least for stock objects. You can still cheerfully perform whatever after market additions you want after the object has been returned of course.
  • There may or may not be GC issues in IE6. Though this could probably be added as a comment to any piece of JavaScript code.
This is basically where I am at the moment in my JavaScript thinking with respect to class declaration. I hope if you've made it this far you've been at least mildly amused by my rant, regardless of whether you find it heretical or not.

All opinions and spelling/grammar mistakes are my own fault and certainly don't represent the opinions and/or spelling/grammar mistakes of my large multi-national employer.

Regards

Andrew