Tuesday, April 13, 2010

JavaScript object inheritance with parents

There are different ways to inherit functionality in JavaScript, including using mixins (mixing in all the properties of one object into another object) and the use of prototypes.

In Dojo, there is dojo.mixin for doing mixins, and dojo.delegate for inheriting properties via prototypes. dojo.delegate is like ECMAScript 5/Crockford's Object.create(), but with a dojo.mixin convenience call.

I really like the dojo.delegate or a Object.create+dojo.mixin combination for inheriting, but it makes it hard to call methods you override from your parent. I see this problem show up frequently with widgets, which typically inherit from each other:

var MyWidget = Object.create(BaseWidget);

//BaseWidget also defines a postCreate method,
//But we want our widget to do work too.

MyWidget.prototype.postCreate = function () {
//Call BaseWidget's implementation
BaseWidget.prototype.postCreate.apply(this, arguments);

//Do MyWidget's postCreate work here.
};
Not too bad, but the BaseWidget.prototype.postCreate.apply junk is a bit much to type, and it gets a bit trickier when there are mixins that also contribute to the functionality.

In Dojo, there is dojo.declare() that helps with this by defining an "inherited" method that can be used to find the BaseWidget's postCreate:

var MyWidget = dojo.declare(BaseWidget, {
postCreate: function () {
//Call BaseWidget's implementation
this.inherited("postCreate", arguments);

//Do MyWidget's postCreate work here.
}
});
This is an improvement as far as typing, but the implementation of dojo.declare has always scared me. My JavaScript Fu is not strong enough to follow it, and I am concerned it is actually a bit too complicated.

So here is an experiment on something simpler:

var MyWidget = object("BaseWidget", null, function (parent) {
return {
postCreate: function () {
//Call BaseWidget's implementation
parent(this, "postCreate", arguments);

//Do MyWidget's postCreate work here.
}
};
});
Here is the implementation of that object function, and here are some tests. That implementation is wrapped in a RequireJS module, but it can be extracted as a standalone script.

The second argument to the object() function allows for specifying mixins.

With two mixins, mixin1 and mixin2, the parent for MyWidget would be an object that inherits from BaseWidget with mixin1 and mixin2's properties mixed in:

var MyWidget = object("BaseWidget", [mixin1, mixin2], function (parent) {
return {
postCreate: function () {
//Call BaseWidget's postCreate, but if it
//does not have a postCreate method, mixin1's
//postCreate function will be used. If mixin1
//does not have an implementation, then mixin2's
//postCreate function will be used. If mixin2 does
//not have an implementation an error is thrown.
parent(this, "postCreate", arguments);

//Do MyWidget's postCreate work here.
}
};
});
dojo.declare has the concept of calling a method called "constructor" if it is defined on the declared object, whenever a new object of the MyWidget type is created. I preserved that ability in object() but the property name for that function is "init" in the object() implementation.

The object() implementation is simpler than dojo.declare, but still gives easy access for calling a parent implementation of a function. It is not has powerful as dojo.declare -- dojo.declare has the concept of postscript and a preamble and even auto-chaining calls. However, I feel the simplified approach is better. It is clearer to follow the code, and to predict how it will behave. I also expect it to perform better.

I like the object() method because it uses closures and a function that accepts the parent function as an argument. Feels very JavaScripty. The prototype chain is a bit longer with the extra object.create() calls creating some intermediate objects, but I expect prototype walking is fast in JavaScript, particularly when you go to measure it in comparison to any DOM operation.

Are there ways in which the object() function is broken or insufficient? Is there a better way to do this? Or even a different way, something that does not rely on a parent reference?

There is traits.js, for using traits. Alex Russell experimented with a trait implementation inside dojo.delegate. Kris Zyp pointed out that Alex's implementation does not have conflict detection or method require support.

I like the idea of mixing in just part of a mixin or remapping a method to fit some other API's expectations, so I can see adding support for the remapping features, similar to what Alex does in the dojo.delegate experiment. However, I am not sure how valuable conflict detection or method require support is.

I can see in large systems it would help with detecting errors sooner, but then maybe the bigger problem is the complexity of the large system. And there is a balance to forcing strictness up front over ease of use. The trait.js syntax looks fairly wordy to me, and the extra benefit of the strictness may not be realized for most web apps.

Also, I do not see an easy way to get the parent reference. It seems like you need to remap each overridden parent function you want to call to a new property name. It seems wordy, with more properties hanging off an object. And do you need to make sure you do not pick a name that is already in use by an ancestor? Seems like it could lead to a bunch of goofy names on an object.

Reusing code effectively is an interesting topic. The traits approach is newer to me, and I keep wondering if there is a better way to do it. It has been fun to experiment with alternatives.

3 comments:

  1. I'm thinking of adopting this object inheritance method in my webapps. Are you still using it? Or did you find any problems?

    ReplyDelete
  2. Andre, I have not had a chance to use it on a "real" project yet, just the unit tests I have for it. I am happy to fix bugs on it if you use it and find issues. It is an object approach I want to explore further.

    ReplyDelete
  3. James, thank you very much. I will use it indeed, and I'll let you know if I find any issues.

    ReplyDelete

Note: Only a member of this blog may post a comment.