Erik's blog

Code, notes, recipes, general musings

re-scoping javascript callback functions using call and apply

leave a comment »

Preamble:
jQuery assigns the this object in an event handler to the element that the event handler is bound to. For example, the following will log the element with the id arbitrary-id:

$("#arbitrary-id").click(function (e) { console.log(this); });

But what if we don’t want this to be bound the element we just clicked on? We can get the element anyways by referring to e.currentTarget. What if we want to assign this to an arbitrary element?

Problem:
Develop a function, accessible as a method on any function, that will allow us to set the element this refers to inside the function. For example, this code will log the element with the id my-foo-div:

$("#arbitrary-id").click(function (e) { console.log(this); }.bind( $("#my-foo-div") ));

Solution:
We need this method to be available on any function. We can add it to the Function prototype to accomplish this.

As an aside, why do we all cringe whenever anyone mentions doing anything to an object’s prototype? Are there really no safe use-cases? It seems like too powerful a tool to ignore completely.

But anyway, here’s the start of a method definition:

Function.prototype.bind = function (element) { console.log(element); };

We can call it like this:

( function () {} ).bind( $("#arbitrary-id") );

How to define the this object inside the callback function called by the jQuery click handler?  Hmm … Well, the callback function is just a function and we did just add a bind method to all functions, so we can start there. What if we just assign this to the element passed in, like so:

Function.prototype.bind = function (element) { this = element; console.log(this); };

Okay, that threw “ReferenceError: Invalid left-hand side in assignment”, so let’s try the call method, which exists to set the scope of a function at the time it’s called.

Function.prototype.bind = function (element) { console.log(this); };
( function () { console.log( this ); } ).call( $("#my-foo-div") ).bind( $("#my-foo-div") );

Things are starting to get seriously weird, and it looks like call doesn’t return a function anyway, which means we can’t chain bind to it. What are we doing again? Oh, yeah, basically, we want to be able to call call on a function, but have the function be runnable later as a callback, something like a deferred call.

We can at least get bind to return a function, so the function’s callable later:

Function.prototype.bind = function (element) { return function () { console.log(element); } };

Now if I run $("#arbitrary-id").click(function (e) { console.log(this); }.bind( $("#my-foo-div") ) );, the callback logs the element with id my-foo-div. So, I have a reference to my-foo-div inside the callback for the event handler attached to arbitrary-id. I think this is progress. I just need to get the callback function to run in the scope of my-foo-div and I think I can do this with call.

Function.prototype.bind = function (element) { return function () { console.log( this.call( element ) ); } };

I’m getting an error “Uncaught TypeError: Object # has no method ‘call'”, which makes sense because jQuery is still setting this to the element that the event handler is attached to, which is the whole point of this exercise, but we’re close! I need to get this to refer to the callback function itself, not a dom element. Inside the bind function definition, this refers to the caller, which is the callback function. Let’s cache this in the scope of the callback function, and then refer to the cached object inside the returned function:

Function.prototype.bind = function (element) { var that = this; return function () { console.log( that ); } };

Nice! When I click on the arbitrary-id element, I see “function (e) { console.log(this); }”, which is my stringified callback function, in the log. So now I just need to call that function via the call method, passing in the element I want to bind the scope to:

Function.prototype.bind = function (element) { var that = this; return function () { that.call( element ); } };

Yay! It works. Before calling it quits, I’d like to be able to pass arguments to the callback. Fortunately, call‘s cousin apply and the native arguments object makes this easy:

Function.prototype.bind = function (element) { var that = this; return function () { that.apply( element, arguments ); } };

This calls for an image to chill out to. Here’s a picture of a toucan doing his thing:

Toucan Three by Rhea Monique

Toucan Three by Rhea Monique

Advertisements

Written by Erik

January 6, 2011 at 6:00 pm

Posted in code

Tagged with , , ,

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: