Have you met arguments.callee?
Just the other day I had a need to use arguments.callee
and I realized that's
not something you really see every day in JavaScript. Maybe I could talk about it a bit.
Anonymous functions everywhere
It's not news to anyone reading this blog that one of JavaScript's workhorses are anonymous functions. Callbacks, strategies, deferred execution, event handlers, etc. They just seem to be all over the place — and for a good reason; they can be convenient and reduce the pollution of a bunch functions that are only called from a single spot.
Another nice thing is that, once your eyes are trained to ignore the little bit of noise that they add to the code, the code is really readable and, dare I say it, expressive.
Yet another contrived example
Let's say we are really into reinventing the wheel and with our understanding
of anonymous functions we create a revolutionary map
function:
function map(array, compute){ var result = [ ]; for(var i=0; i < array.length; i++){ result.push( compute(array[i]) ); } return result; }
This function, as you can hopefully see, transforms each element of the given array into something else and returns the array of the transformed elements. Two simple uses are shown below.
//apply discount var prices = [1, 2, 3, 4]; var discount = 0.1; // 10% off today, w00t! var newPrices = map(prices, function(price){ return (1-discount)*price; } ); //==> [0.9, 1.8, 2.7, 3.6] //compute areas var squareSides = [1, 2, 3, 4]; var squareAreas = map(squareSides, function(side){ return side*side; } ); //==> [1, 4, 9, 16]
I warned you the examples would be contrived, didn't I?
Now your product manager comes and asks for a page where the users
can enter a list of numbers and get the factorials for each of them.
You immediately think your friend the map
function will
save the day. You start and...
//return the factorials var userNumbers = [1, 2, 3, 4]; var factorials = map(userNumbers, function(number){ if(number <= 1) { return 1; } return number * ????????(number - 1); // ooops! I need to recurse here. } );
You see, there's this thing with anonymous functions. They don't have a name, d'oh. As I said in the beginning, we typically use them in situations where they are called only once so we can inline them and forego the need fora name. But now we're kind of wishing they had a name.
Anonymity won't hide you from me
Well, if the post tittle didn't already give it away, we can achieve that
with
arguments.callee
. Using arguments.callee
inside a function gives
us a reference to the function itself. So now we can finish our code.
//return the factorials var userNumbers = [1, 2, 3, 4]; var factorials = map(userNumbers, function(number){ if(number <= 1) { return 1; } return number * arguments.callee(number - 1); //or if you were using "this" in the function you'll probably want to: // return number * arguments.callee.apply(this, [number - 1]); } ); //==> [1, 2, 6, 24]
A more real world scenario
I won't leave you without at least a reference to a real use case for this feature. The example I'll show comes from Nicholas Zakas. In a blog post a while ago he showed how we can break up long running tasks with smaller timed/deferred chunks, improving the browser's responsiveness.
Here's the function from his blog post, which process chunks of an array for 50ms, then stops and call itself back to process the remaining items soon after that — giving the browser a chance to breathe and take care of its interaction with the user
//Copyright 2009 Nicholas C. Zakas. All rights reserved. //MIT Licensed function timedChunk(items, process, context, callback){ var todo = items.concat(); //create a clone of the original setTimeout(function(){ var start = +new Date(); do { process.call(context, todo.shift()); } while (todo.length > 0 && (+new Date() - start < 50)); if (todo.length > 0){ setTimeout(arguments.callee, 25); } else { callback(items); } }, 25); }
I hope this shows you a little new trick.