3

My understanding of JS and jQuery is pretty limited, and I come from a C# background. But I do know what variable capture is, and I know that if I capture a variable inside the loop that was declared OUTSIDE of a loop, each time the delegate runs I'll get the last value of the captured variable, not the one at the time of the capture. However, this is clearly not the issue here, yet I still receive the last value:

for (var i = 0; i < dialogs.length; i++) {

    var dialog_button = dialogs[i];

    var ix_parts = $(dialog_button).attr("id").split("_");
    var index_tag = ix_parts[1];
    var dialog_panel = $(dialog_panel_selector.replace("$ix$", index_tag));
     $(dialog_button).click(function (event) {
            $(dialog_panel).dialog('open');
            return false;
        });

}

Since dialog_button was declared inside the loop's scope, I'd expect that I'll receive the correct value inside the click handler.
Is JS doing something different?

TDaver
  • 7,164
  • 5
  • 47
  • 94
  • possible duplicate of [Assign click handlers in for loop](http://stackoverflow.com/questions/4091765/assign-click-handlers-in-for-loop) – Quentin Nov 20 '11 at 10:26
  • There is no "variable declared in loop" and "variable declared outside the loop". JS has only function scope. That is, every `var foo = ...` inside the function is a bit of a semantic mistake, since actually `var foo` is always at the beginning of the function and `foo = ...` is in code. –  Nov 20 '11 at 10:29

2 Answers2

10

JavaScript doesn't have block scope, only function scope (well, and global scope). Closures receive a live reference to the variable, and so your event handler function (which is a closure) will always see the last value assigned to dialog_button.

Your best bet in the specific situation you describe is to use jQuery's $.each function rather than a for loop (thank you @Esailija, and note that Tadeck suggested $.each before I did — worth an upvote — I added it on @Esailija's suggestion because it's by far the better solution in this specific situation):

$.each(dialogs, function(index, dialog_button) {

    var ix_parts = $(dialog_button).attr("id").split("_");
    var index_tag = ix_parts[1];
    var dialog_panel = $(dialog_panel_selector.replace("$ix$", index_tag));
    $(dialog_button).click(function (event) {
         $(dialog_panel).dialog('open');
         return false;
    });
});

...because now, each call to the function we're passing $.each gets its own unique dialog_button argument, so each generated function (closure) closes over its own copy and we don't run into the problem of new values being assigned to the variable.

I've suggested the jQuery function because you're using jQuery already, and so you have it available for certain. As of ECMAScript5, there's a native Array#forEach function that does much the same thing, but not all engines have it yet.

In situations where the above doesn't fit the bill, here's another way to go. It also includes a fairly in-depth discussion of what's happening, why, and how to control it to your advantage:

Your best bet is to use a function that creates the event handlers, like so (I assume all of this is within a function):

for (var i = 0; i < dialogs.length; i++) {

    var dialog_button = dialogs[i];

    var ix_parts = $(dialog_button).attr("id").split("_");
    var index_tag = ix_parts[1];
    var dialog_panel = $(dialog_panel_selector.replace("$ix$", index_tag));
    $(dialog_button).click(createHandler(dialog_button));
}

function createHandler(dlg) {
    return function (event) {
        $(dlg).dialog('open');
        return false;
    };
}

There, the loop calls createHandler, which creates the handler function as a closure over the context of the call to createHandler, so the handler refers to dlg. Each call to createHandler will get its own unique context, and thus its own unique dlg argument. So the closures refer to the expected values.

You can position createHandler inside your overall function if you like (make sure it's not inside any branches, it must be at the top level of the function), like this:

function createDialogs() {
    for (var i = 0; i < dialogs.length; i++) {

        var dialog_button = dialogs[i];

        var ix_parts = $(dialog_button).attr("id").split("_");
        var index_tag = ix_parts[1];
        var dialog_panel = $(dialog_panel_selector.replace("$ix$", index_tag));
        $(dialog_button).click(createHandler(dialog_button));
    }

    function createHandler(dlg) {
        return function (event) {
            $(dlg).dialog('open');
            return false;
        };
    }
}

...or if you need to do the same thing elsewhere, you might move it out a level:

function createDialogs() {
    for (var i = 0; i < dialogs.length; i++) {

        var dialog_button = dialogs[i];

        var ix_parts = $(dialog_button).attr("id").split("_");
        var index_tag = ix_parts[1];
        var dialog_panel = $(dialog_panel_selector.replace("$ix$", index_tag));
        $(dialog_button).click(createHandler(dialog_button));
    }
}

function createHandler(dlg) {
    return function (event) {
        $(dlg).dialog('open');
        return false;
    };
}

The advantage to the latter is it means a small block of memory (called the variable binding object) created by each call to createDialogs is not referenced by anything and can be cleaned up when the call returns, whereas with the former (where createHandler is within createDialogs), that memory is referenced by the variable binding objects for the calls to createHandler, and so isn't eligible for cleanup. Each has its uses, it just depends on whether you need access to anything in the createDialog call context (you don't in the code you've shown, but I realize it's just an excerpt).

More reading:


In the comments you've asked:

"(make sure it's not inside any branches, it must be at the top level of the function)" can you elaborate on this? I've declared the function inside the for loop, and it seems to be working! Am I doing something wrong?

JavaScript has two different function constructs: Function declarations and function expressions. Syntactically they're very similar, but they're legal in different places and they happen at different times.

TL;DR: My createHandler is an example of a function declaration. They cannot be within control structures. Your function construct that you're passing into click is a function expression, which can be. The difference is whether the function construct is a right-hand value or not (mine isn't, yours is). Properly understanding declarations vs. expressions is key to skilled JavaScript programming.

The long version:

Here's a function declaration:

function foo() {
}

Here's a function expression being assigned to a variable:

var foo = function() {
};

Here's another function expression, this time being used as a property initializer in an object literal:

var obj = {
    foo: function() {
         }
};

And another function expression, this time being passed into a function as an argument:

bar(function() {
});

As you can see, the distinction between them is that function declarations stand alone, whereas a function expression is used as a right-hand value in a containing expression — as the right-hand side of an assignment (=) or initializer (:), or passed into a function as an argument.

Function declarations are processed when the scope containing them is created, before any step-by-step code is executed. So given:

function bar() {

    function foo() {
    }

    return foo;
}

...when bar is called, before any step-by-step code runs, the foo function is created. Only then is the step-by-step code run, which in this case returns a reference to the foo function. Consequently, the above is exactly equivalent to this:

function bar() {

    return foo;

    function foo() {
    }
}

Even though it looks like foo shouldn't exist as of the return statement, it does.

Since function declarations happen before the step-by-step code, they cannot be within control flow statements:

function bar(condition) {
    if (condition) {
        function foo() {    // <== INVALID
            return alert("A");
        }
    }
    else {
        function foo() {    // <== INVALID
            return alert("B");
        }
    }

    return foo;
}
var f = bar(true);
f(); // alerts what?

You'd think that the alert would say "A", right? Because we passed true for condition and so the first branch occurs. But that's not what happens at all. Technically, the above is a syntax error, pure and simple. But most browsers don't treat it as one (Firefox's engine [SpiderMonkey] is the only one I know of that does). So what do they do? It depends. Most engines continue to treat those as function declarations, and when you have two function declarations for the same function in the same scope, the specification says the second one wins. So those engines will alert "B". But other engines (IE is one) rewrite your code on-the-fly, turning those declarations into expressions, and so those engines will alert "A". Here There Bye Dragons. Don't do this. :-)

Function expressions, on the other hand, create functions as part of the step-by-step code. They happen when execution reaches them in the flow. So this is very different than the previous example:

function bar() {
    var foo;

    return foo;

    foo = function() {
    };
}

Here, bar returns undefined, because as of the return statement, foo is undefined and of course the following assignment statement never occurs. Similarly, this is valid and its behavior is well-defined:

function bar(condition) {
    var foo;

    if (condition) {
        foo = function() {
            return alert("A");
        };
    }
    else {
        foo = function() {
            return alert("B");
        };
    }

    return foo;
}
var f = bar(true);
f(); // alerts "A"
f = bar(false);
f(); // alerts "B"

Because now we're using function expressions, they occur as of the step-by-step code, and the behavior is as it seems it should be.

Bringing this all back home to your specific example: In general, it's a bad idea to create functions in loops, but there are times when it's necessary. On those occasions, usually you want a helper like my createHandler function, which is outside the loop, so you're controlling context better. You could do this:

for (var i = 0; i < dialogs.length; i++) {

    var dialog_button = dialogs[i];

    var ix_parts = $(dialog_button).attr("id").split("_");
    var index_tag = ix_parts[1];
    var dialog_panel = $(dialog_panel_selector.replace("$ix$", index_tag));
    $(dialog_button).click((function(dlg) {
        return function (event) {
            $(dlg).dialog('open');
            return false;
        };
    })(dialog_button));
}

...but it's a very bad idea. Firstly, it's hard to read. Secondly, you create extra functions: Every loop iteration actually creates two function objects, the one we're using to create the other (e.g., the one that was createHandler), and the one it creates. So if there are three dialogs, you create six functions instead of three, and all of those function stick around until the handlers are removed.

And a final note: All of the function expressions I've shown above have created anonymous functions (functions with no names). I don't like anonymous functions; giving functions names helps your tools help you. Technically, it's legal to give them names:

var f = function foo() { // <== WARNING, see below
};

...but you can't do that in the wild at the moment because of bugs in IE prior to IE9. IE8 and below will see that construct twice, once as a function declaration, then again as a function expression. It really will create two function objects, which can cause all sorts of trouble. :-)

Community
  • 1
  • 1
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • I think you should do : `$(dialog_button).click((createHandler(dialog_button))());` you need on the start to execute the func so it will give you a new one to execute – Royi Namir Nov 20 '11 at 10:18
  • @RoyiNamir: What I have is correct. Your code would call `createHandler` to create the handler function, and then *execute* the returned handler function immediately, passing its return value into `click`, which is not what we want. We want to call `createHandler` to create the function object and return a reference to it (without calling it), and then pass the reference into `click` so it can get set as an event handler. When the click occurs, the handler function will get called. – T.J. Crowder Nov 20 '11 at 10:25
  • (make sure it's not inside any branches, it must be at the top level of the function) -> can you elaborate on this? I've declared the function inside the for loop, and it seems to be working! Am I doing something wrong? – TDaver Nov 20 '11 at 10:26
  • @TDaver: In JavaScript, there's a significant distinction between function *declarations* and function *expressions*, I'll add a note to the answer. :-) – T.J. Crowder Nov 20 '11 at 10:27
  • +1 so it doesn't rot in the bottom but wish you had recommended `forEach`/`jQuery.each` because they just are the most KISS solution here :-) – Esailija Nov 20 '11 at 10:32
  • @Esailija: You are **1,000%** right. I really, really ought to have done that and will go back and do so now since this answer's been accepted. I see Tadeck's already suggested it, I'm off to upvote. – T.J. Crowder Nov 20 '11 at 10:56
6

Closures and for loop

Yes, it is doing something different, which is proven by the following (and this jsfiddle):

var tests = [1,2,3,4,5];

for (var i=0; i<tests.length; i++){
    var test = tests[i];
};

alert(test);

The above will alert last value from tests array, which is exactly 5. This is caused by the fact, that for loop is not a closure - variables defined in it are accessible also outside of it.

jQuery.each() as possible solution

By the way, jQuery has jQuery.each() helper function that will help you iterate through objects and arrays without the need for for loop. And inside the callback local variables will remain (be) local.

Solution

So, basically, the following should solve your problems (however it is untested, so please test it first):

jQuery.each(dialogs, function(ind, dialog){
    var dialog_button = $(dialog);
    var index_tag = dialog_button.attr("id").split("_")[1];
    var dialog_panel = $(dialog_panel_selector.replace("$ix$", index_tag));
    dialog_button.click(function(event){
        event.preventDefault();
        dialog_panel.dialog('open');
    });
});

Did it help? Do you have any questions?

Tadeck
  • 132,510
  • 28
  • 152
  • 198