2

I am using a third-party JavaScript library that has a certain long-running function (it involves web service calls over the network, etc). For simplicity, let's say it takes one parameter, a callback function to call when the long-running operation completes, so let's say we have the signature longRunningFunction(callback).

Unfortunately, the function does not accept a "context" parameter, so if I call longRunningFunction multiple times, when my callback is called I have no way of knowing which call resulted in which callback.

I found the following way to solve this, by using anonymous functions: Define a mycallback(context) function, and then do something like this every time I invoke the long-running operation:

uniqueContext = getUniqueContextFromSomewhere();
longRunningFunction(function() {mycallback(uniqueContext)});

This seems to work, but my question is whether or not this is guaranteed to work according to the JavaScript spec in all possible circumstances, given that the long-running operation may be executing on a different thread, callbacks to the various calls to longRunningFunction may come in any order, etc. So, is the solution I found valid?

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
Eugene Osovetsky
  • 6,443
  • 2
  • 38
  • 59
  • 1
    Are you asking how clousures work? http://stackoverflow.com/questions/111102/how-do-javascript-closures-work – epascarello Jan 28 '13 at 18:02
  • 1
    You probably want: [`longRunningFunction(myCallback.bind(uniqueContext));`](https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind). If you're using underscore: [`longRunningFunction(_.bind(myCallback, uniqueContext));`](http://underscorejs.org/#bind), or jQuery: [`longRunningFunction($.proxy(myCallback, uniqueContext));`](http://api.jquery.com/jQuery.proxy/) – zzzzBov Jan 28 '13 at 18:08
  • 1
    I strongly disagree with closing this as an "exact duplicate" of the closures question. For someone who is not a JS expert, it is not at all straightforward how the discussion there maps to an answer to this question. I can probably figure it out, but not sure that I will get it right. On the other hand, there are a lot of JS developers (including beginners) using 3rd-party libraries using the async callback pattern, and so StackOverflow would benefit from having a clear answer to this specific question. (Having said that, the link to the closures question is of course useful - thanks!) – Eugene Osovetsky Jan 28 '13 at 18:15
  • zzzzBov: Could you make your comment into an answer and explain why the "bind" approach is better? – Eugene Osovetsky Jan 28 '13 at 18:16

2 Answers2

2

You can provide another context using Function.prototype.bind(thisArg [, arg1 [, arg2, …]]). bind returns a new method that, when called, has its this keyword set to thisArg. In your case:

longRunningFunction(mycallback.bind(uniqueContext));

This does not work in older browsers (IE less than 9 for example) but you could use a polyfill or library like Underscore.js. To learn more about bind I reccomend the following resources:

Sven
  • 913
  • 8
  • 16
  • Thanks - this helps but I would still like a fuller answer. Is the "bind" approach better than the approach I wrote about in my original question, and if so - why? What if I don't want to change the value of "this"? Is the approach I wrote about in my question guaranteed to work in all scenarios, and if not - when will it fail (and will the "bind" approach work in these circumstances?) – Eugene Osovetsky Feb 04 '13 at 19:55
2

Assuming this code :

function longRunningFunction( fn ) {
    window.setTimeout( fn, 1000 ); // just to make it async
}

function myCallback( context ) {
    /* ... */
}

Now if you have this :

var uniqueContext = getUniqueContext( );
longRunningFunction( function ( ) { myCallback( uniqueContext ); } );

It is specified by the standard that the context parameter of myCallback will be uniqueContext at the moment where the callback will be fired. It can cause some issues when you try something like this in a loop (because you will erase uniqueContext at each iteration).

If you do this :

var uniqueContext1 = getUniqueContext1( );
longRunningFunction( function ( ) { myCallback( uniqueContext1 ); } );

var uniqueContext2 = getUniqueContext2( );
longRunningFunction( function ( ) { myCallback( uniqueContext2 ); } );

The callback using uniqueContext1 is garanteed to be called when the first longRunningFunction will end, and the one using uniqueContext2 when the second longRunningFunction will end (with the same restriction than before; if you overwrite uniqueContext1 or uniqueContext2 somewhere in your scope, the callback parameter will change too).

Using bind allows you to avoid creating a closure (and the default previously mentioned). The following code is similar to the previous one, except that you will never be able to accidentally change the value of the parameter by overwriting the previous one :

longRunningFunction( mycallback.bind( null, getUniqueContext1( ) ) );
longRunningFunction( mycallback.bind( null, getUniqueContext2( ) ) );

mycallback.bind( null, someParameter ) will return a function which, when called, will call mycallback with null as this (which will fallback to window like any regular function) and someParameter as first parameter.

The other answer is using only the first parameter of bind because they use the this variable, but it is not required and you can safely use function parameters instead.

Maël Nison
  • 7,055
  • 7
  • 46
  • 77
  • Thanks! One more question... you say that the value of "context" will be "uniqueContext" at the moment where the callback will be fired. What if, at the time the callback is fired, "uniqueContext" has already gone out of scope (i.e. the function that calls longRunningFunction has already returned)? According to spec, will it be the last value of uniqueContext before it goes out of context, or undefined, or something else? – Eugene Osovetsky Feb 05 '13 at 16:15
  • Even if you returns from the function which calls longRunningFunction, uniqueContext will still be available until you explicitely or implicitely remove all references of this value. You don't have to worry about this, the value will not be destroyed if you leave the scope. – Maël Nison Feb 05 '13 at 20:23