6

I have this callback function setup:

var contextMenu = [];
var context = [ { "name": "name1", "url": "url1" }, {"name": name2", "url: "url2" } ];
for(var i=0; i < context.length; i++) {
    var c = context[i];
    var arr = {};
    arr[c.name] = function() { callback(c.url); }
    contextMenu.push( arr );
}
function callback(url) {
   alert(url);
}

The problem is that the url value passed to the callback is always the last value in the context variable - in this case "url2". I am expecting to pass specific values to each "instance" of the callback, but as the callback seems to be remember the same value, the last time it was referred.

I am kind of stuck. Any help would be appreciated.

PS: I am using jQuery ContextMenu which, to my understanding, does not support sending custom data to its callback functions. It is in this context that I have this problem. Any suggestions to overcome in this environment is also helpful!

Anurag
  • 140,337
  • 36
  • 221
  • 257
rsmoorthy
  • 2,284
  • 1
  • 24
  • 27
  • Possible duplicate of [this post](http://stackoverflow.com/questions/1331769/access-outside-variable-in-loop-from-javascript-closure) and dozens of others – Wayne Feb 28 '11 at 16:11

3 Answers3

16

Use an additional closure.

arr[c.name] = (function(url) { 
    return function() { callback(url); }
})(c.url);

See Creating closures in loops: A common mistake and most other questions on this topic, and now your question is also added to this pool.

Community
  • 1
  • 1
Anurag
  • 140,337
  • 36
  • 221
  • 257
  • Thanks! I was mystified by "closures" many times and now it is getting more cleared with some hands-on experience! – rsmoorthy Feb 28 '11 at 17:00
  • Closures can be awesome, and especially since functions are first-class objects in JavaScript, its kind of hard to do without closures. Imagine passing every single variable being referenced in the function as an argument - in this case both `callback`, and `url` and all the headaches of whether the argument was passed by value or reference. – Anurag Feb 28 '11 at 17:47
  • I think it's worth noting that it's not the closure per-se that makes it work, but more specifically that this closure code includes passing an argument - which makes for a passing-by-value in place of the reference kept in the original closure, thus overcoming the original problem. I think this makes the explanation more complete. – matanster Dec 14 '13 at 02:43
3

You are creating a series of closure functions inside the for loop

arr[c.name] = function() { callback(c.url); }

and they all share the same scope, and hence the same c object which will point to the last element in your array after the loop finishes.

To overcome this issue, try doing this:

arr[c.name] = function(url) {
    return function() { callback(url); };
}(c.url);

Read more about closures here: http://jibbering.com/faq/notes/closures/

ArtBIT
  • 3,931
  • 28
  • 39
0

General solution

Callback creator helper

I created a general callback creator along the Creating closures in loops: A common mistake that Anurag pointed out in his answer.

Parameters of the callback creator

  • The function's first parameter is the callback.
  • Every other parameter will be passed to this callback as parameters.

Parameters of the passed callback

  • First part of the parameters come from the arguments you passed to the callback creator helper (after the first parameter as I described previously).
  • Second part comes from the arguments that will be directly passed to the callback by its caller.

Source code

//Creates an anonymus function that will call the first parameter of
//this callbackCreator function (the passed callback)
//whose arguments will be this callbackCreator function's remaining parameters
//followed by the arguments passed to the anonymus function
//(the returned callback).
function callbackCreator() {
    var functionToCall = arguments[0];
    var argumentsOfFunctionToCall = Array.prototype.slice.apply(arguments, [1]);
    return function () {
        var argumentsOfCallback = Array.prototype.slice.apply(arguments, [0]);
        functionToCall.apply(this, argumentsOfFunctionToCall.concat(argumentsOfCallback));
    }
}

Example usage

Here is a custom AJAX configuration object whose success callback uses my callback creator helper. With the response text the callback updates the first cell of a row in a DataTables table based on which row the action happened, and prints a message.

{
    url: 'example.com/data/' + elementId + '/generate-id',
    method: 'POST',
    successHandler: callbackCreator(function (row, message, response) {//Callback parameters: Values we want to pass followed with the arguments passed through successHandler.
            table.cell(row, 0).data(JSON.parse(response).text);
            console.log(message);
        },
        $(this).parents('tr'),//Row value we want to pass for the callback.
        actionName + ' was successful'//Message value we want to pass for the callback.
    )
}

Or in your case:

arr[c.name] = callbackCreator(function(url) {
        callback(url);
    },
    c.url
);
Community
  • 1
  • 1
totymedli
  • 29,531
  • 22
  • 131
  • 165