jQuery context parameter

2011-01-04

Lately I seem to be venting quite negatively about this framework called jQuery. I'm not sure where "lately" comes into the picture, but I won't argue about it. Where I'm generally fine with jQuery, the concept and it's usage, there are some parts of it I truly despise. Mostly I try to keep this to myself. Although I do vent most of my fury on IRC, where it's very much appreciated ;)

So for the TL;DR's amongst you, here's the deal.

I'd like to see an additional parameter for jQuery's click(func) method (and all the other event methods). The parameter should accept an optional context. When a context is supplied it should become the context of the callback when called. I have three reasons for wanting this over bind() (or $.proxy()):

1. It's simpler
2. It's cleaner
3. It's faster

Below I'll explain these points in some detail. End of TL;DR.

The jQuery event handler methods like click() and mousemove(), or more generally bind(), are sugar for attaching events. Nothing wrong with that, in fact it's very common. And when the event fires, the context or this is set to the DOM element. (Not the jQuery object being targeted! Although I've heard sounds that this should be changed.) That's pretty standard and how it would be when doing it "natively" (through document.addEventListener etc).

But when working with js objects, in a proper OO fashion, context becomes important. You might create a new object to wrap around some GUI concept. Then you might create some logical methods for handling events. But when attaching these to the proper events you have to choose your context.

Even though the context for such events is normally bound to the DOM element that caught the event, when working with objects I feel it makes much more sense to set the context to the object that would wrap the DOM element that fired the event.

To fix this context, there are a few tools available in js. The three main choices here are closures, bind and call/apply. Here are examples of each:

Code: (js)
// closure:
var that = this; // self
$e.click(function(e){ that.onClick(e); });

// bind:
$.click(this.onClick.bind(this));
// or the jQuery way
$.click($.proxy(this.onClick, this));

// call/apply
$.click(this.onclick, this);

Yes, call and apply don't show up in this example, I'm aware. Yet, it's what that choice would look like. Let's look at each case individually.

The closure pattern is very common. It's probably the easiest to understand too. You create a variable to which the function remains to have access even when the original code returns. Then when the event fires, the proper context is set for calling the method and all is fine. Now as I said initially, my fix has three improvements. for this pattern #1 does not cause a problem. The concept is simple and easy to grasp when beginning with js. You don't even have to actually know about closures or context to accept that it works the way it does. #2 should be obvious, since your polluting your scope with an extra variable. The callback also has the function(){} bloat. It also fails #3 because it's using a closure and the lookup cost makes this solution slower. My main concern for this example is #2.

The bind pattern is very uncommon. It's common amongst those very familiar with js, but other than them it's an underused pattern. And while I'm perfectly happy with bind in most cases, it does not suffice in this case. Interesting fact, the bind pattern is actually just syntactic sugar for the closure pattern. A simple implementation may look something like this...

Code: (js)
Function.prototype.bind = function(ctxt){
var f = this;
return function(){
f.call(ctxt);
// or even something like (yes yes, I know)
// f.apply(ctxt, Array.slice(arguments));
};
};

This pattern boggled my mind the first time I saw it. Black voodoo magic! But it's easy to grasp as, like I said, it's exactly the same pattern as the closure pattern. Note that ES5's bind does a little more than this. Also note that it seems like $.proxy, which is jQuery's bind method, does way more than just this. But even if you can simply use a native bind, I still object (in this case!). It fails #1, I'll have to explain the concept of bind and therefore the whole concept of context to whomever might be involved in the project. I have no problem with explaining js to people, but a project at work is not the place to do it. It obviously scores much better at #2 than the closure pattern since it's just sugar for that pattern. That's the point of sugar :p However, it's still wrapping the object with $.proxy( ... ), which still obfuscates legibility. Especially when not very acquainted with jQuery. Now I know most people using jQuery are very familiar with the concepts of js, but let's be reminded of the few that don't. So that leaves #3, which has exactly the same drawback as the closure pattern, except it has a "but". "But", a native bind might be optimized to negate the overhead with more voodoo. Since the performance overhead is the least of my worries in this case, it's not very compelling to me.

Then we have the call/apply pattern. This would mean jQuery exposes a click(func, ctxt) fingerprint for these events. Since these methods are already overloaded (to fire clicks), I don't think that should be a problem. The ctxt would be optional. When available, it would simply call call or apply on the callback with a different context.

I'm going to assume that it actually already does use call/apply since it's wrapping a DOM objet. But even if they don't, it wouldn't matter. It's trivial to implement:

Code: (js)
$.prototype.click = function(){
// (do other overload stuff here or something)
...
else if (typeof arguments[0] == 'function' && typeof arguments[1] == 'object') {
// called with click(func, obj)
// do magic stuff here.
// callback now, our dom element is in var e
func.call(obj, evt, e);
}
};

That would be the cleanest of them all. The call would be $e.click(function(){}, this);, which is very clean and is about as optimized as you can get with the whole generic framework paradigm.

As for #1, it's simple and straightforward. You simply give the context objects as a parameter. You don't have to explain anything if you don't want to. "Just supply this as the second parameter and it'll be fine". And all the while it's still intuitive to use this in the callback. Both when the function is defined inline another method or when it's itself a method of the object. I doubt anyone will be confused by the fact this does not point to the DOM object.

Regarding #2, obviously $e.click(function(){}, this); is very clean. This is especially the case when inlining a longer function and would otherwise be using $.proxy. Using bind is cleaner since .bind(this) can simply be appended at the end, minifying the visual impact about as much as the call/apply pattern does (but still a little more, although arguably it's more obvious what's going on to seasoned developers unfamiliar with jQuery). Some examples:

Code: (js)
$e.click(this.click, this);
$e.click(function(){..}, this);
$e.click(function(){
this.a();
this.b();
this.c();
}, this);

// vs

$e.click(this.click.bind(this));
$e.click(function(){..}.bind(this));
$e.click(function(){
this.a();
this.b();
this.c();
}.bind(this));

// vs *ugh*

$e.click($.proxy(this.click, this));
$e.click($.proxy(function(){..}, this));
$e.click($.proxy(function(){
this.a();
this.b();
this.c();
}, this));

So that brings us to #3. While a closure is still being made by click (etc), this is also the case for the other two patterns. So for them, it's even a double (or maybe even triple) level closure. So those cancel each other out and the other two patterns are have always one more level of closure..ness. A call is just a property lookup (usually at least one level up the prototypal chain) which is probably faster than at least one level of the scope chain.

But please note that the whole speed thing is a relatively minor issue. Especially with events, I doubt the loss in overhead is ever noticeable. I guess it's more an issue of clean code and legibility.

Anyways. I've tried to make my case. This is why I'd like to see a jQuery.prototype.click(func, ctxt) version of all the event hooking methods.

And yes, that's .prototype, thank you.

Oh and while you're at it, why don't you just rewrite .bind to .on and preserve .bind for something that actually reflects the language ktnx.