1

I just needed to use partial application to pass data into a callback, and I found I needed a layer of indirection to prevent subsequent runs of a loop from changing the data I'm passing to the callback.

See here for a very simple working example: http://jsfiddle.net/s5gVj/

(if the jsfiddle is self explanatory, feel free to skip straight to the question below)

Without the indirection

This will always set the label to "Button 1 pressed."

This happens regardless of which button was pressed.

for(var i = 0 ; i < buttons.length ; i++)
{
    var buttonID = i;
    $(buttons[i]).click(   
        function(e)
        {
            $("label").html("Button " + buttonID + " pressed.");
        });
}

Indirectly, via a function

Meanwhile, using a function in the middle corrects the problem

For the first button, the result will be "Button 0 pressed"

For the second button, it will be "Button 1 pressed".

var buttonClickHandlerWithID = 
    function(buttonID)
    {
        return function(e)
        {
            $("label").html("Button " + buttonID + " pressed.");
        }
    }

for(var i = 0 ; i < buttons.length ; i++)
{
    $(buttons[i]).click(buttonClickHandlerWithID(i));
}


Why does this happen? Is there something special about function calls that ensure the variable is copied and thus no longer a reference to the same data, or is there something else going on?

I expected a variable declared within the for loop to be recreated on each iteration, thus each iteration being separate, but I guess that isn't the case?

Finn
  • 45
  • 4
  • 1
    instead of adding `buttons.length - 1` number of `click` events - you might wanna consider a delegation, i.e. add a single click event to the parent container and filter targets by button class... – Dziad Borowy Jun 30 '14 at 16:11
  • @tborychowski The buttons in question are generated by jQueryUI, and I don't really want to fiddle with them in a breakable way (they are in the button list it can create for a dialog). So they share the same classes, i could detect their position, but I find this preferable. – Finn Jun 30 '14 at 17:10
  • @Bergi quite right, it would have answered my question, thanks. Though the answers here require less prior knowledge, I think. (I'd forgotten about function/block scope, which might have made it harder to interpret the answers there). – Finn Jun 30 '14 at 17:18
  • @tborychowski though maybe sharing the click event is possible whilst storing state seperately? I'll investigate. Thanks for the tip. – Finn Jun 30 '14 at 17:19

2 Answers2

2

JavaScript only has function scope. All var declarations anywhere in a function (well except inside a nested function of course) are treated as if they appeared at the top of the function. Nested { ... } blocks do not have their own scope, which is a significant difference from C++ and Java etc.

Pointy
  • 405,095
  • 59
  • 585
  • 614
1

Like Pointy answered, there is only function scope in javascript, meaning i in your code is scoped outside the loop, and when referenced later (on click) will be whatever it ended up being after the for loop is finished.

If you use jquery's .each() instead of for, you get local scope because it takes a function as callback.

var i = 0;
var directButtons = $("button.direct");

// jquerys each method takes a function callback
directButtons.each(function () {
    i++;
    // since this is inside a function
    var buttonID = i; // buttonID will be locally scoped

    $(this).click(function (e) {
        // this inner function "close over" (google "javascript closure") and
        // remembers the value of buttonID in the .each() loop
        $("label").html("Button " + buttonID + " pressed. " +
          "'i' will be whatever it ended up after the .each() is finished: " + i);
    });
});

http://jsfiddle.net/s5gVj/3/

Also check out slide 55 of fixingthesejquery.com


You can also solve this using an Immediately Invoked Function Expression (see the part titled "Saving state with closures"):

var directButtons = $("button.direct");

for(var i = 0 ; i < directButtons.length ; i++){
    (function(buttonID){
        // buttonID is now scoped to this function
        $(directButtons[buttonID]).click(function (e) {
            $("label.direct").html("Button " + buttonID + " pressed. , 'i' is always " + i);
        });
    })(i); // call the function immediately, supply the value of "i" as parameter
}

http://jsfiddle.net/s5gVj/4/

xec
  • 17,349
  • 3
  • 46
  • 54