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.