I was answering a JavaScript question on stackoverflow when a common usage scenario presented itself, one with a subtle gotcha that could catch you out. Perfect for a quick blog post. (Note: you could also view this post as an adjunct to my popular JavaScript callback posts (I, II, III).)
My solution to the problem involved adding some extra precondition code to an existing event handler. The event was the plotclick
event of the jQuery Flot charting library, so the original event handler code looked like this:
$("#chart").bind("plotclick", function (event, pos, item) { // respond to the click });
Pretty easy stuff. However in my solution I wanted to enhance the event handler so that some preconditions were checked and, if they passed, the original code could be executed. Simple enough: just add the preconditions to the code – which is what I did for the answer.
$("#chart").bind("plotclick", function (event, pos, item) { if (item) { var dataPoint = item.series.data[item.dataIndex]; if (dataPoint[2]) { // respond to the click } } });
For one chart, meh, not a problem. Make the change and move on. For several, it gets a little clunky; all those complicated bits of cut-n-paste. What would be better would be to wrap the original function – somehow – and then call the wrapper. In other words, in my mind I was thinking of something like this:
$("#chart").bind("plotclick", wrap(function (event, pos, item) { // respond to the click }));
So that the minimal amount of change would be required. This would also work if the event handler were not an anonymous function.
What would this wrap
function look like? Well, it takes a function with the required signature (that is, takes three parameters) and returns another function, the event handler, that is also of the required signature and that calls the original function. (Call these the wrapper and the wrapped.)
var wrap = function (originalHandler) { return function (event, pos, item) { // extra stuff originalHandler(event, pos, item); }; };
There is, however, one small problem with this code. I will admit that the first time I wrote it I missed the issue, and it was only through testing that I discovered the bug, so it’s not glaringly obvious.
I’ll stop a moment for you to think about it.
Found it? Give yourself a bonus point if you did. The problem is the this
variable, the context of the wrapper, is not passed on to the wrapped. As written, the call to originalHandler
is a function invocation, and the this
variable inside the function will be the global object, not the context passed in for the wrapper function. With jQuery, the context will be the DOM element for which the event was triggered, and my code just blithely throws that away.
Let’s rectify that right now:
var wrap = function (originalHandler) { return function (event, pos, item) { // extra stuff originalHandler.call(this, event, pos, item); }; };
In other words, we use the Function call
method to pass on the this
variable on to the called function.
So, going back to my particular case, I’d have:
var wrap = function (originalHandler) { return function (event, pos, item) { if (item) { var dataPoint = item.series.data[item.dataIndex]; if (dataPoint[2]) { originalHandler.call(this, event, pos, item); } } }; };
So, I’d say the takeaway from this blog post is that—yet again—the fundamental concept that functions are objects (that is, you can pass them around, and create functions that return functions), and that you should be careful if you do so that you take care of the this
variable, if needed.
Now playing:
Orbit, William - Lark (alex metric remix)
(from Pieces In A Modern Style 2)
2 Responses
#1 configurator said...
08-Jul-11 9:11 AMUse originalHandler.apply(this, arguments); - this way if you have extra arguments (other than event, pos, item) they're still forwarded to the originalHandler.
#2 julian m bucknall said...
08-Jul-11 10:39 AMConfigurator: Excellent point. I agree.
Cheers, Julian
Leave a response
Note: some MarkDown is allowed, but HTML is not. Expand to show what's available.
_emphasis_
**strong**
[text](url)
`IEnumerable`
* an item
1. an item
> Now is the time...
Preview of response