views:

147

answers:

5

Closures in a loop are causing me problems. I think I have to make another function that returns a function to solve the problem, but I can't get it to work with my jQuery code.

Here is the basic problem in a simplified form:

function foo(val) {
  alert(val);
}

for (var i = 0; i < 3; i++) {
  $('#button'+i).click(function(){
    foo(i);
  });
}

Naturally clicking on any of the three buttons will give an alert saying 3. The functionality I want is that clicking on button 1 will give an alert saying 1, button 2 will say 2 etc.

How can I make it do that?

+4  A: 

See the bind method.

$('#button'+i).bind('click', {button: i}, function(event) {
  foo(event.data.button);
});

From the docs:

The optional eventData parameter is not commonly used. When provided, this argument allows us to pass additional information to the handler. One handy use of this parameter is to work around issues caused by closures

Andy E
Thanks, it works.
Rob
+2  A: 

Use the .each function from jquery - I guess you a looping through similar elements - so add the click using something like:

$(element).children(class).each(function(i){
   $(this).click(function(){
      foo(i);
   });
});

Not tested but I always use this kind structure where possible.

matpol
+1  A: 

Or just manufacture a new function, as you describe. It would look like this:

function foo(val) {
    return function() {
        alert(val);
    }
}

for (var i = 0; i < 3; i++) {
    $('#button'+i).click(foo(i));
}

I'm pretty sure Mehrdad's solution doesn't work. When you see people copying to a temporary variable, it's usually to save the value of "this" which may be different within an inner child scope.

darkporter
Yes his solution didn't work, I think he deleted it.
Rob
This is the best answer I know of. The code with bind is fine, but that data parameter is really an ugly hack. This approach has the advantage that it'll work any time you have this problem (creating closures that refer to a loop variable), whether you have jQuery or not.
Jason Orendorff
@Jason: while I agree this is a great answer, and certainly the answer I would have given if jQuery weren't mentioned and tagged in the question, I have to disagree on it being an "ugly hack" - it's certainly neater. Also, a semi-similar `Function.bind` is in the 5th edition specification, so you could say the best answer for plain ol' js would have been the equivalent prototyping method that would provide that functionality in current editions of js. http://snipplr.com/view/13987/functionbind/ is just one example I could find, I know there are many.
Andy E
Oh, the data parameter strikes me as a hack because it's using C-style poor man's closures to work around problems with real closures. Which seems kind of sad. Anyway, they're all fine, and the main problem, I think, is one they all share: without a comment, it's never obvious enough to readers who haven't seen this particular problem before *why* we're jumping through this extra hoop instead of closing on `i` directly. :-\
Jason Orendorff
A: 

@Andy solution is the nicest. But you can also use Javascript scoping to help you save the value in your closure.

You do so by creating a new scope in your loop body by executing an anonymous function.

for (var i = 0; i < 3; i++) {
  (function(){
    var index = i; 
    $('#button'+index).click(function(){
      foo(index);
    });
  })();
}

Since the loop body is a new scope at each iteration, the index variable is duplicated with the correct value at each iteration.

Vincent Robert
thanks that's useful to know
Rob
+2  A: 

Try this code:

function foo(val) {
  alert(val);
}

var funMaker = function(k) {
  return function() {
    foo(k);
  };
};

for (var i = 0; i < 3; i++) {
  $('#button'+i).click(funMaker(i));
}

Some important points here:

  • JavaScript is function scoped. If you want a new ('deeper') scope, you need to create a function to hold it.
  • This solution is Javascript specific, it works with or without jQuery.
  • The solution works because each value of i is copied in a new scope as k, and the function returned from funMaker closes around k (which doesn't change in the loop), not around i (which does).
  • Your code doesn't work because the function that you pass to click doesn't 'own' the i, it closes over the i of its creator, and that i changes in the loop.
  • The example could have been written with funMaker inlined, but I usually use such helper functions to make things clearer.
  • The argument of funMaker is k, but that makes no difference, it could have been i without any problems, since it exists in the scope of the function funMaker.
  • One of the clearest explanation of the 'Environment' evaluation model is found in 'Structure and Interpretation of Computer Programs', by Sussman & Abelson (http://mitpress.mit.edu/sicp/ full text available online, not an easy read) - see section 3.2. Since JavaScript is really Scheme with C syntax, that explanation is OK.

EDIT: Fixed some punctuation.

Miron Brezuleanu
The same as darkporter's answer, but with some nice exposition. *"JavaScript is really Scheme with C syntax"* Every time someone understands this for the first time, a JS angel gets his wings.
Jason Orendorff