Tuesday, April 8, 2008

Function.prototype Fun!

In my previous post I extended Function.prototype. Since functions are fundamental components of JavaScript, I thought it worth showing a few more extensions that have proven useful.
Function.prototype.bind = function (obj)
{
var method = this;
var f = function () { return method.apply(obj, arguments); }
return jscore.addMeta(f, { binding : "bind", target : method });
};

Function.prototype.bind2 = function (obj)
{
var method = this;
var f = function () {
var args = jscore.arrayCat([this], arguments);
return method.apply(obj, args);
};
return jscore.addMeta(f, { binding : "bind2", target : method });
};

Function.prototype.head = function ()
{
var method = this, first = jscore.arrayCat(arguments);
var f = function () {
var args = jscore.arrayCat(first, arguments);
return method.apply(this, args);
};
return jscore.addMeta(f, { binding : "head", target : method });
};

Function.prototype.tail = function ()
{
var method = this, last = jscore.arrayCat(arguments);
var f = function () {
var args = jscore.arrayCat(arguments, last);
return method.apply(this, args);
};
return jscore.addMeta(f, { binding : "tail", target : method });
};

Function.prototype.returns = function (ret)
{
var method = this;
var f = function () { method.apply(this, arguments); return ret; };
return jscore.addMeta(f, { binding : "returns", target : method });
};

Function.prototype.seal = function ()
{
var method = this;
var f = function () { return method(); };
return jscore.addMeta(f, { binding : "seal", target : method });
};

And the helper functions:
$namespace("jscore", {

addMeta : function (obj, meta)
{
if (!obj.$meta)
obj.$meta = {};
for (var n in meta)
obj.$meta[n] = meta[n];
return obj;
},

arrayCat : function ()
{
var ret = new Array(jscore.arrayLen.apply(null, arguments));
for (var i = 0, k = 0; i < arguments.length; ++i)
for (var a = arguments[i], n = (a ? a.length : 0), j = 0; j < n; ++j)
ret[k++] = a[j];
return ret;
},

arrayLen : function ()
{
var ret = 0;
for (var a, i = 0; i < arguments.length; ++i)
if (a = arguments[i])
ret += a.length;
return ret;
}

}); // jscore

While the basic goal of these methods is to help create closures more cleanly and efficiently, one of them has proven to be by far the most useful: bind.

If you haven't used closures before, you are really missing out on the power of JavaScript. But, if you have used closures, you've probably experienced their dark side: memory leaks, especially in IE.

This is a well known problem for IE, but these methods can help that situation. Even outside of IE, closures can hold on to objects that aren't needed, thus producing a resource leak or at least a less-than-desirable cost. The problem with closures stems from the fact that the closure implicitly keeps references to all variables in its lexical scope, whether it needs them or not. In other words:
function foo (x, y)
{
var a = [ ... ]; // lots of stuff
document.getElementById("bar").onclick = function () { ... }
}

In the above code, the onclick handler function captures a, x and y and holds on to them for its entire life. If the function doesn't need all of these objects, their persistence is wasteful. In this situation, head (or tail) can be used to reduce the bound variables to only what is needed. For example,
function clickHandler (x, y)
{
...
}
function foo (x, y)
{
var a = [ ... ]; // lots of stuff
document.getElementById("bar").onclick = clickHandler.head(x, y);
}

Now, the garbage collector can cleanup a, but clickHandler can still reference x and y. More importantly, this helps in IE in the following example:
function foo (x, y)
{
var a = [ ... ]; // lots of stuff
var el = document.getElementById("bar");
...

el.onclick = function () { ... }
}

The above code illustrates a classic memory leak situation in IE. The problem has already been well explained, but here's how head helps:
function clickHandler (x, y)
{
...
}
function foo (x, y)
{
var a = [ ... ]; // lots of stuff
var el = document.getElementById("bar");
...

el.onclick = clickHandler.head(x, y);
}

With this approach, neither clickHandler nor the closure function created by head keep a reference on the el variable. This removes the reference cycle and avoids the memory leak.

That is all nice, but bind has an even more useful feature. In fact it is this feature that inspired me to write these methods in the first place. It was only after some use that I decided to add the ability to store parameter values, because (to me) the most important was bind.

In my past work, programming in C++ was always complicated by the need to preserve the blasted this pointer. This is especially problematic when working with C API's that make no such accomodations for tracking state. Fast forward a bit. When I started programming in JavaScript, I noticed it felt a lot like C. I had functions, and I had data, but often enough I had no good way to write methods that were associated with an object.

The browser interface is full of places where this pointers are easily lost. Take setTimeout for example. It can call a function after a specified amount of time. Well and good, but where's my bloody this pointer?!? If setTimeout and its partner setInterval were alone, I may not have sought a general solution. Sadly, they are far from alone. Even in the above examples, the onclick event handler does not preserve any this pointer. Even worse, DOM elements call these handlers with themselves as the this pointer! Talk about frustrating, but I'll get back to events another time.

Once or twice I've had the need to connect a method that processed all provided arguments (like arrayCat and arrayLen) to a caller that would supply an extra parameter or two. You can imagine the confusion that caused! At first, I threw in a closure and then realized the error of my ways. OK, it was a memory leak that caused me to realize my mistake. So, there's a pair of methods that are unlikely to ever get more complex: returns and seal.

Together returns and seal fixed my memory leak. The seal method cuts off any supplied arguments that it receives and calls the bound function. The returns method took care of returning the proper value.

Of course, these are really cool in combination:
function foo ()
{
for (var i = 0; i < arguments.length; ++i)
...
};

function bar (x, y)
{
var el = document.getElementById("foobar");
var fn = foo.head(x, y).seal().returns(false);
el.onclick = fn;
};

Though highly dubious, this example illustrates chaining head, seal and returns. Together, these methods can go a long way to allowing arbitrary methods to be connected without playing with the fire of naked closures.

Lastly, these binding methods required a slight tweak to the getName method. This ensures that it will report its correct name as well as the real target method to which it is bound.
Function.prototype.getName = function ()
{
if (!this.$meta)
{
var name = this.name || this.toString().match(/function (\w*)/)[1];
name = name || "~anonymous~";
this.$meta = { fullname : name, name : name };
}

var s = this.$meta.fullname;
if (this.$meta.target)
s += "~"+this.$meta.binding+">" + this.$meta.target.getName();
return s;
}

Enjoy!

No comments: