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.
hugoware.net is licensed under a