jQuery is, quite rightly, lauded for providing a way of coding JavaScript for web browsers that just works. It pushes you relentlessly towards the pit of success, making simple things easy and difficult things possible. But if there's one niggle I have with coding jQuery, it's the anonymous function noise level. Every block of jQuery code is wrapped up in
$(function() {
...
});
and inside that you're constantly having to nest yet more anonymous blocks:
$(function() {
$('div.clickable').click(function() { ... }).
};
(Aside: I see a lot of people coding jQuery who don't use the $(function) shorthand to hook up document ready handlers. There's NO NEED to write $(document).ready(function() { ... }) - $(function() {}) is exactly the same, and way shorter.)
To be fair, it's largely the fault of JavaScript. Given that it's the basic unit of scope in the language, the amount of syntax you need to wrap around a function in JS is excessive. We can't do much about that directly, but there might be something we can do to help in some of jQuery's cases. Let's use some higher-ordered functional programming tricks and see if we can eliminate some functions - if that makes any sense.
Here's the theory:
Most of the time, what you end up doing inside each of these anonymous functions is creating a new jQuery object with a call to the $() function, passing it a selector, or frequently 'this', then chain a series of function calls onto that. Here's a simple example:
$(function() {
$('.clickMe').click(function() {
$(this).toggleClass('clicked');
});
});
There's two event handler functions here - one handling document.ready, and one handling a click.
Let's start with a little refactoring. Let's pull the functions out into variables, so we can look at them:
var handleClick = function() {
$(this).toggleClass('clicked');
};
var setupClickHandler = function() {
$('.clickMe').click(handleClick);
};
$(setupClickHandler);
That is (modulo some scope variations that aren't relevant) exactly equivalent to the inline anonymous function version.
But it seems to me that those two functions have a very similar structure to one another. Both of them call $(), then call a method on the result. Would it be possible, perhaps to have some sort of clever function that can build functions like that for us?
With JavaScript's support for first-class functions, the answer is 'yes, absolutely'. Let's build one. Here's step one:
var magicFunctionBuilder = function(selector) {
return function() {
$(selector || this);
};
};
This is just a starting point, to get us into the idea of a function that builds a function for us. Look closely at what is does: it returns a function that calls the jQuery function passing either the selector we give it, or 'this' if we omitted the selector.
So if we call it thus:
var f = magicFunctionBuilder('.clickMe');
f now contains a function, which, when called, calls $('.clickMe'). It's equivalent to our having written:
var f = function() { $('.clickMe'); };
only with a lot less curly braces.
Similarly:
var g = magicFunctionBuilder();
is equivalent to
var g = function() { $(this); };
But we need our magically-built function to not only call the $() function for us; we need it to chain on the subsequent series of calls we need to perform on the jQuery object. How can we tell it what series of function calls to make?
Well, we could describe calling .toggleClass('clicked') with an object, like this:
var call = {
method: 'toggleClass',
args: ['clicked']
};
Given an object, o, and an array of call objects like that, calls, we can persuade JavaScript to call them in sequence, chaining the return value in as the 'this' for the next call. Here's how:
var executeCalls = function(o, calls) {
for (var i = 0; i < calls.length; i++) {
var call = calls[i];
var fn = o[call.method];
o = fn.apply(o, call.args);
}
return o;
};
This uses the apply method, which all JavaScript functions possess, which calls the function as a method of the supplied object, with the supplied argument list.
Now, if our magicFunctionBuilder had access to an array of calls, it could build us a more sophisticated function. Let's try it:
var magicFunctionBuilder = function(selector, calls) {
return function() {
var o = $(selector || this);
executeCalls(o, calls);
};
};
Now, we can write this:
var handleClick = magicFunctionBuilder(null, [{ method: 'toggleClass', args: ['clicked'] }]);
That is exactly equivalent to our original
var handleClick = function() { $(this).toggleClass('clicked'); };
But we've clearly swapped one complexity for another one here. What's needed is a better way to build that array of call objects.
JavaScript functions all have access to an arguments object. It contains a few interesting properties, but usefully it also provides indexed access to all the arguments supplied to the function. You can extract the values into an array very easily:
var argArray = function(args) {
var arr = [];
for (var i = 0; i < args.length; i++) arr[i] = args[i];
return arr;
};
Now imagine a function like this:
var createToggleClassCall = function() {
return {
method: 'toggleClass',
args: argArray(arguments)
};
};
Now our handleClick creation could be a little cleaner:
var handleClick = magicFunctionBuilder(null, [createToggleClassCall('clicked')]);
Now for the clever bit. Instead of passing in a fully-formed array of calls to the magicFunctionBuilder, we can instead add methods onto the function it returns (yes, we can add methods onto a function object - welcome to the crazy world of Extreme JavaScript) that add calls for us. For example, we could do this:
var magicFunctionBuilder = function(selector) {
var calls = [];
var fn = function() {
var o = $(selector || this);
executeCalls(o, calls);
};
fn.toggleClass = function() {
calls.push({
method: 'toggleClass',
args: argArray(arguments)
});
};
return fn;
};
Let's just rename magicFunctionBuilder to something a little more terse:
var $$ = magicFunctionBuilder;
and NOW we can create handleClick like this:
var handleClick = $$().toggleClass('clicked');
which is, remember, equivalent to our original
var handleClick = function() { $(this).toggleClass('clicked'); };
So all that's left to do now is to provide more than just a toggleClass method. It would be nice to provide all the methods that the user will expect to find on the actual jQuery object itself, don't you think? Well, conveniently, we can find those by enumerating $.fn's members - the members of the jQuery object prototype - finding all the functions, and making dummy 'recorder' versions of our own:
var $$ = function(selector) {
var calls = [];
var fn = function() {
var o = $(selector || this);
executeCalls(o, calls);
};
for (var member in $.fn)
{
if (! ($.fn[member] instanceof Function)) continue;
(function(method) {
fn[method] = function() {
calls.push({
method: method,
args: argArray(arguments)
});
return fn;
};
})(member);
}
return fn;
};
So now, as well as handleClick, we can build our other function, which you may remember, looked like this:
var setupClickHandler = function() {
$('.clickMe').click(handleClick);
};
Rewritten with our magic builder:
var setupClickHandler = $$('.clickMe').click(handleClick);
And we can now remove our original refactoring out of the variables, since our code's so much terser:
$(function() {
$('.clickMe').click(function() {
$(this).toggleClass('clicked');
});
});
becomes
$(
$$('.clickMe').click(
$$().toggleClass('clicked')
)
);
And to show some of the power of it, let's do something more complicated, including some chaining and animation completion callbacks:
$(
$$('.clickMe').click(
$$().toggleClass('clicked')
.hide('slow',
$$().show('slow',
$$().toggleClass('clicked')))
)
);
with anonymous functions, this becomes the monstrous:
$(function() {
$('.clickMe').click(function() {
$(this).toggleClass('clicked')
.hide('slow', function() {
$(this).show('slow', function() {
$(this).toggleClass('clicked');
});
});
});
});
We can even make $$() Visual Studio intellisense friendly by lying to intellisense and telling it it returns a jQuery object; we'll get full intellisense on all the functions we can access.
I can't be sure this is an original idea, and I'm sure I've missed some subtlety that means this breaks in some common scenarios. I'm also not 1oo% happy with the $$ naming choice, nor sure I'd actually use this in a live dev environment (the maintenance coders would likely kill me). But it's certainly food for thought.