TDaver
TDaver

Reputation: 7264

jQuery variable capture in loop

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?

Upvotes: 3

Views: 2851

Answers (2)

T.J. Crowder
T.J. Crowder

Reputation: 1074485

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. :-)

Upvotes: 10

Tadeck
Tadeck

Reputation: 137380

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?

Upvotes: 6

Related Questions