Extending Functions in Javascript
Lately I’ve been writing a lot of Javascript, definitely a trend that I expect to continue for the foreseeable future. While working on a recent project I came across the following problem:
var condition = another_condition = true; // For this example to work!
function animate (callback)
{
c = (typeof(callback) == 'function') ? callback : function () {};
if (condition) {
c = function () {
c();
console.log('bar');
};
}
if (another_condition) {
c = function () {
// More functionality.
console.log('baz');
c();
};
}
c();
}
animate(function () { console.log('foo'); });
My intent was to allow a function reference to be passed into animate(), and modify the reference so that additional functionality would be added depending on the evaluation of several conditions.
Assuming that both condition and another_condition evaluated true I had expected the output of the above to be:
baz foo bar
Of course this isn’t what happened. The output was something like:
baz baz baz baz baz baz baz …
…before my browser crashed.
What happened?
What happened was simple, yet it’s cause was an obscurity of Javascript closures; the variable c changed to reference 3 different functions during the course of this code execution. Initially it pointed to the function reference passed into animate, however when condition, and later another_condition evaluated true, c was reassigned once, then again to now be a reference to the later newly created anonymous function.
I was aware than due to the phenomenon that is closures that each newly created function would have full access to c and be able to call the function directly. What I didn’t expect was that this function reference would not be dynamic and not necessarily reference the function that c referenced at the time the function was created.
So when c was called, all it ended up doing was calling itself recursively, repeatedly logging ‘baz’, and eventually overflowing and crashing Firefox.
The Solution
Obviously references to c would need to be stored somewhere for later retrieval. The solution was to store a reference to the current value of c in a local anonymous namespace.
var condition = another_condition = true; // For this example to work!
function animate (callback)
{
c = (typeof(callback) == 'function') ? callback : function () {};
if (condition) {
(function () {
var oldC = c;
c = function () {
oldC();
console.log('bar');
};
})();
}
if (another_condition) {
(function () {
var oldC = c;
c = function () {
console.log('baz');
oldC();
};
})();
}
c();
}
animate(function () { console.log('foo'); });
Not the neatest Javascript example I’ve ever written, but nevertheless this is an very powerful javascript pattern.
My particular use case was a fairly complex animation routine in which sections of the routine could be turned on or off. The callback passed in would be augmented in this fashion in order to ensure it would execute when the animation was complete.
However the solution above isn’t very scaleable; the code repetition seems to suggest that a useful abstraction could be added.
Function.prototype.extendFn = function (fn) {
var c = this;
return function () { fn(c); };
};
// Alternative, for those who don't like modifying Function.prototype:
var extendFn = function (callback, fn) {
var c = callback;
return function () { fn(c); };
};
The original code can now be rewritten:
var condition = another_condition = true; // For this example to work!
function animate (callback)
{
c = (typeof(callback) == 'function') ? callback : function () {};
if (condition) {
c = c.extendFn(function (c) {
c();
console.log('bar');
});
}
if (another_condition) {
c = c.extendFn(function (c) {
console.log('baz');
c();
});
}
c();
}
animate(function () { console.log('foo'); });
And will work as expected, printing:
baz foo bar
Much better! This solution was perfect for my particular use case, and I can see it being a useful functional programming pattern.
Update: Apologies for not mentioning this earlier, but thanks to the folks on Freenode's #javascript IRC channel, in particular joekarma for his original solution and suggestions. This post came out of a conversation yesterday in which we discussed possible solutions similar to those given above.
4 Comments
Well, the problem you are describing is (at least in my opinion) a classic case where the Decorator design pattern would apply.
Now, I haven't had the chance to program in JavaScript a lot (other than several poorly implemented functions :), and I am looking forward to learn/write some 'real' code in it) but JavaScript is, after all, an object oriented language, and I guess you could investigate further for an objects-based solution (maybe a little more readable for JavaScript beginners :P).
Anyway - I think that the basic idea of the pattern is already implemented - so, nice job!
For some reason, the fact that you simultaneously called the solution I offered over IRC sloppy *and* took credit for it is causing my blood to boil. If my memory serves me correctly, I also pointed out that extending Function.prototype was one method of abstracting away the repetition. The least you could have done was included a single sentence to thank, or at least acknowledge, the people on freenode who helped you solve this issue.
Ok, I'm pretty sure the word sloppy doesn't appear anywhere above!
But full credit to you, please note the edit made above. Apologies for not mentioning this in the original post, I wrote this post hastily yesterday and didn't take the time to source it correctly.
In my defense nowhere did I take credit for it, this is simply a description of the problem I encountered and the solution that was uncovered. I'm hoping someone with a similar solution will find some use in it :)
Apology accepted.I read "not exactly neat" to be synonymous with "sloppy", but perhaps there are those who don't see the same negative connotations in the phrase as I."Not the neatest Javascript example I’ve ever written" (emphasis mine) sure seems like taking credit for authorship to me. I'm not denying that you wrote most of that script -- it's just that you wrote it in its broken (not to mention contrived) form. If you had placed that little qualification after the code you wrote entirely yourself, I would not have had a problem with this post.I don't want to pick on you specifically. What you did is not exactly grievous. It just sucks to realize that despite the fact I've written page-long scripts for individuals, for free and in the spirit of being helpful, I probably haven't been credited for any of it. Such is life on the Internet.For what it's worth, I don't find the look of anonymous wrapper functions particularly unpleasant, even if their purpose is opaque to those less versed in the arcane features of JavaScript (can I still call closures an arcane feature)? The syntax isn't the greatest, but it's easy enough to get used to.
Have something to say? Post a comment!
If you have a Gravatar linked to your email address it will be displayed along with your comment.
Please say something constructive in your post. Rants are fine (and somewhat encouraged!), but anything that is plainly offensive or abusive will be removed.