2
for (id = 50; id < 100; id++)
{
    if($('#'+id).attr('class') == 'myField')  
    {
            $('#'+id).bind('click', function() { install(id); } );
    }
}

No idea why id can't reach 'install' in function(). I am trying to bind every button (id from 50 to 100) with a click event to trigger the install(id) function. But it seems the variable id cannot reach install function. While I hard code it:

for (id = 50; id < 100; id++)
{
      if($('#'+id).attr('class') == 'myField')  
      {
            $('#'+id).bind('click', function() { install( 56 ); });
      }
}

it works! Please tell me why.

Richard J. Ross III
  • 55,009
  • 24
  • 135
  • 201
Newbie
  • 2,775
  • 6
  • 33
  • 40
  • Why you just don't get id attribute inside of `install` function? in your case it has the same value as provided variable. – Samich Nov 18 '11 at 13:09
  • 1
    http://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example – Francis Nov 18 '11 at 13:09
  • There is `.hasClass` by the way. – pimvdb Nov 18 '11 at 13:41
  • possible duplicate of [Access outside variable in loop from Javascript closure](http://stackoverflow.com/questions/1331769/access-outside-variable-in-loop-from-javascript-closure) – Felix Kling Nov 18 '11 at 13:46

5 Answers5

4

What you made is one of the most common mistakes when using Javascript closures.

By the way the very fact that this mistake is so common is IMO a proof that it's indeed a "bug" in the language itself.

Javascript supports read-write closures so when you capture a variable in a closure it's not the current value of the variable that is captured, but the variable itself. This means that for example in

var arr = [];
for (var i=0; i<10; i++)
    arr.push(function(){alert(i);});

each of the 10 functions in the array will contain a closure, but all of them will be referencing the same i variable used in the loop, not the value that this variable was having at the time the closure was created. So if you call any of them the output will be the same (for example 10 if you call them right after the loop).

Luckily enough the workaround is simple:

var arr = [];
for (var i=0; i<10; i++)
    arr.push((function(i) {
                return (function(){alert(i);});
              })(i));

using this "wrapping" you are calling an anonymous function and inside that function the variable i is a different one from the loop and is actually a different variable for each invocation. Inside that function i is just a parameter and the closure returned is bound to that parameter.

In your case the solution is therefore:

for (id = 50; id < 100; id++)
{
    if($('#'+id).attr('class') == 'myField')  
    {
        $('#'+id).bind('click',
           (function(id){
                 return (function() { install(id); });
            })(id));
    }
}
6502
  • 112,025
  • 15
  • 165
  • 265
3

By not reaching the install(), I guess you mean you get all your install(id) behaves like install(100).

Reason why it doesn't work

This is caused by the javaSctipt closure. This line function() { install(id) } assign the id to the install() callback function. The id's value won't be resolved until install() is call when is far later after the loop is finished - the time when id has already reached 100.

The solution is create another closure the hold the current id value.

for (id = 50; id < 100; id++)
{
    if($('#'+id).attr('class') == 'myField')
    {
            (function (id) {
              $('#'+id).bind('click', function() { install(id); });
            }) (id);

    }
}

Here is a demonstration code:

var funcCollections = [];
for (id = 50; id < 100; id++)
{
    if(true)
    {
            (function () {
              var thatId = id;
              funcCollections.push(function () {console.log(thatId,id)});
            }) ();

    }
}

// funcCollections[1]();
// 51 100
// undefined
// funcCollections[2]();
// 52 100
steveyang
  • 9,178
  • 8
  • 54
  • 80
  • I tried this solution but it doesn't seem to be working. Are you sure that's correct? – melihcelik Nov 18 '11 at 13:22
  • @melihcelik I made a mistake, you need to create a closure to make it work. See edited post. – steveyang Nov 18 '11 at 13:25
  • It's still not quite right. Instead of `var thatId = id;` inside the inner function, pass the outer "id" as a parameter to the function. – Pointy Nov 18 '11 at 13:38
  • It won't work as you expect if you pass the `id` to the inner function, as I mentioned the reason in the post. And as you see in the demonstration. `id` is passed to the innerfunction but when the value (which `id` points to) is resolved, it returns 100 (when the loop finishes). All the innerFunction which references `id` will return 100. To make a solution, you need to preserve the `id` in the loop, `var thatId = id` creates a local variable within a closure and preserves **the id** in that loop. Another solution is to use a **local parameter** to hold that `id` value. (Edited in the post) – steveyang Nov 18 '11 at 13:42
  • Ah I see, I misread the code. Well it would actually work if you passed it as a parameter to the anonymous immediately-invoked function, which is what I meant. – Pointy Nov 18 '11 at 13:51
  • Yep, I edited it to include the behavior you mentioned, a little more readable~ :) – steveyang Nov 18 '11 at 13:52
1

You can't pass a variable to the function you've bind. It loses the val. When you pass '56' it will be always 56, but when you pass a var, the JavaScript will not bind the value of the var in the loop.

fonini
  • 3,243
  • 5
  • 30
  • 53
1

When you loop over variables and you create anonymous functions(closure) that reference the loop variable they will reference the last value

also note that you don't limit scope the loop variable to the for loop(it's not declared with var) so that means that later modifications to that variable will be propagated to all closures.

take a look at this

Community
  • 1
  • 1
Liviu T.
  • 23,584
  • 10
  • 62
  • 58
1

It's down to variable scope.

The anonymous function you're binding to the click event of the $('#' + id) elements has no awareness of the id variable in the your sample code (assuming that your sample code is an excerpt from a function). Even if it did (e.g. you declared id outside of any function, giving it global scope), id would hold the value 100 when the click event was called, which isn't what you intend.

However, you could use $(this).attr('id') to get hold of the element's id value instead:

for (id = 50; id < 100; id++)
{
    if($('#' + id).attr('class') == 'myField')  
    {
        $('#' + id).bind('click', function()
        {
            install(parseInt($(this).attr('id')));
        });
    }
}

Check out the jQuery .bind() documentation, it shows how this can be used from within an event handler.

Community
  • 1
  • 1
Richard Ev
  • 52,939
  • 59
  • 191
  • 278