Passing Context With JavaScript
Passing functions as arguments in JavaScript is a very powerful technique but can be problematic.
One of the common problems is ending up an unexpected this
context. It happens quite often - for example, the callback from an AJAX request.
// simple class that performs an async call var Search = function() { // starts an ajax search this.find = function(phrase) { // save info to `this` this.phrase = phrase; // perform a method with a callback and pass // in a defined method for this instance $.ajax({ success: this.display }); }; // will be called at the end of the ajax request this.display = function(results) { // `this.phrase` is undefined console.log(this.phrase); }; };
In this particular instance, this
is the AJAX object created to handle the request, but you don't have to look far
to find other situations. Using window.setTimeout
results with the this
context being the DOMWindow
.
The typical solution would be to declare a variable that keeps track of the correct instance of this
and then refer back to it within the callback. It's not too complicated but it is some extra code.
// rewritten to prevent losing context this.find = function(phrase) { this.phrase = phrase; // keep track of the instance var $this = this; $.ajax({ // use an anonymous function and $this // to identify the correct context success: function(results) { $this.display(results) } }); };
This has worked well enough for some time now, however, I thought I'd try a different approach.
Passing Along Context
Functions in JavaScript already have a couple methods attached to them such as call
and apply
. Why not include another method that can be used to provide context when passing a function as an argument?
// provide context when passing a function Function.prototype.context = function(context) { var action = this; return function() { action.apply(context, arguments); }; }
This little bit of code adds an additional method can be used to provide context to a callback. Additionally, because it's attached to all functions, you can make it work with references to defined functions as well as anonymous functions.
// passing a function reference load( this.display.context(this) ); // or using an anonymous function load( function(){ ... }.context(this) );
So, looking back at the original AJAX call, the final example is much simpler.
// rewritten to use the 'as' method this.find = function(phrase) { this.phrase = phrase; $.ajax({ success: this.display.context(this); }); };
... But, The Original Context?
It's worth mentioning that if you need the original context, for example the request information used by AJAX call example earlier in the post, then you'll need to modify this code to include that context to the callback.
One approach might be to append it as the last argument, for example...
// appends the original context when requested Function.prototype.context = function(context, append_original) { var action = this; return function() { // if needing the original context, add it as the last argument if (append_original) (arguments = [].slice.call(arguments, 0)).push(this); // invoke with the new context action.apply(context, arguments); }; };
This makes it so that the last argument provided to the callback is the original this
context.
$.ajax( success: function(results, ajax) { // ajax : the original `this` context // this : the context $.ajax was called from }.context(this, true /* append context */));
There are probably a few ways you could go about handling this but since I haven't come across a time that I needed it I'm not sure which approach would be best.
In any case, this technique has worked very well so far. I haven't encountered any unexpected issues and the code seems easier to read than before.
April 5, 2012
Passing Context With JavaScript
Simple trick to maintain context when passing functions as arguments.