jQuery循环中的变量捕获

3

我对JS和jQuery的理解相当有限,来自C#背景。但是我知道什么是变量捕获,并且我知道如果我在循环内部捕获一个在循环外部声明的变量,则每次委托运行时都会得到捕获变量的最后一个值,而不是捕获时的值。 然而,显然这不是问题所在,但我仍然收到了最后一个值

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;
        });

由于dialog_button是在循环作用域内声明的,我期望在点击处理程序中会收到正确的值。
JS是否做了不同的事情?


可能是重复的问题:在for循环中分配点击处理程序 - Quentin
在JS中,没有“循环中声明的变量”和“循环外声明的变量”。JS仅具有函数作用域。也就是说,函数内的每个var foo = ...实际上都是语义错误,因为实际上var foo总是在函数开头,而foo = ...则在代码中。 - user1046334
2个回答

10
JavaScript没有块级作用域,只有函数作用域(还有全局作用域)。闭包接收对变量的实时引用,因此您的事件处理程序函数(它是一个闭包)将始终看到分配给dialog_button的最后一个值。
在您描述的特定情况下,最好使用jQuery的$.each函数,而不是for循环(感谢@Esailija,注意Tadeck在我之前建议使用$.each- 值得一赞 - 我根据@Esailija的建议添加了它,因为在这种特定情况下,它是更好的解决方案)
$.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;
    });
});

因为现在,我们传递给$.each函数的每个调用都有自己独特的dialog_button参数,所以每个生成的函数(闭包)都关闭了它自己的副本,我们不会遇到将新值赋给变量的问题。
我建议使用jQuery函数,因为您已经在使用jQuery,所以可以使用它。截至ECMAScript5,有一个本地的Array#forEach函数,它做了很多相同的事情,但并非所有引擎都具备该功能。
在以上情况不适用的情况下,还有另一种方法。它还包括一个相当深入的讨论,介绍了正在发生的事情、原因以及如何控制它以使其更有利于您:
最好的选择是使用一个创建事件处理程序的函数,就像这样(我假设所有内容都在一个函数中):
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;
    };
}

在这里,循环调用createHandler,它将创建处理程序函数作为对createHandler调用上下文的闭包,因此处理程序引用dlg。每次调用createHandler都将获得自己独特的上下文,因此其拥有自己独特的dlg参数。因此,闭包引用了预期的值。
如果愿意,您可以将createHandler放置在整个函数内部(确保它不在任何分支内,而是必须在函数的顶层),如下所示:
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;
        };
    }
}

...或者如果你需要在其他地方做同样的事情,你可以将它移到更高一级:

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;
    };
}

后者的优点是每次调用createDialogs时创建的小块内存(称为变量绑定对象)不被任何东西引用,并且在调用返回时可以清理,而前者(其中createHandler位于createDialogs中)的这些内存由于调用createHandler的变量绑定对象引用,因此不符合清理条件。两者都有其用途,这取决于您是否需要访问createDialog调用上下文中的任何内容(在您展示的代码中并不需要,但我意识到这只是一个摘录)。
更多阅读:
在评论中,你问道:
“(确保它不在任何分支内,它必须在函数的顶层)”你能详细解释一下吗?我已经在for循环内部声明了函数,而且它似乎是可以工作的!我做错了什么吗?
JavaScript有两种不同的函数构造:函数声明和函数表达式。从语法上来说,它们非常相似,但它们在不同的位置合法,并且它们发生的时间也不同。
简而言之:我的createHandler是一个函数声明的示例。它们不能在控制结构内部。你传递到click的function构造是一个函数表达式,可以在其中。区别在于function构造是否是右值(我的不是,你的是)。正确理解声明与表达式对于熟练的JavaScript编程至关重要。
长话短说:
这是一个函数声明:
function foo() {
}

这里有一个将函数表达式赋值给变量的例子:

var foo = function() {
};

这里有另一个函数表达式,这次它作为对象文字中的属性初始化器使用:
var obj = {
    foo: function() {
         }
};

还有另一个函数表达式,这次作为参数传递到函数中:

bar(function() {
});

正如你所看到的,它们之间的区别在于函数声明是独立存在的,而函数表达式作为包含表达式的右值使用 - 作为赋值(=)或初始化器(:)的右侧,或作为参数传递给函数。

函数声明在包含它们的作用域被创建时进行处理,在执行任何逐步代码之前。因此,考虑到:

function bar() {

    function foo() {
    }

    return foo;
}

...当调用bar时,在任何逐步执行代码之前,将创建foo函数。只有在这之后才会运行逐步执行代码,此时返回对foo函数的引用。因此,上述代码与以下代码完全等价

function bar() {

    return foo;

    function foo() {
    }
}

尽管在return语句之后看起来foo不应该存在,但它确实存在。
由于函数声明发生在逐步执行的代码之前,因此它们不能在控制流语句内部。
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?

您可能认为警报会显示"A",对吗?因为我们为condition传递了true,所以第一个分支发生了。但实际上情况并非如此。从技术上讲,上面是语法错误,纯粹而简单。但大多数浏览器不会将其视为错误(Firefox的引擎[SpiderMonkey]是我知道的唯一一个)。那么他们会做什么?这取决于。大多数引擎继续将它们视为函数声明,当您在同一作用域中有两个相同函数的函数声明时,规范指定第二个函数获胜。因此,这些引擎将警报"B"。但其他引擎(IE就是其中之一)会即时重写您的代码,将这些声明转换为表达式,因此这些引擎将警报"A"。龙在这里,再见。不要这样做。 :-)

另一方面,函数表达式会将函数创建为逐步执行的代码的一部分。它们在执行流程到达它们时发生。因此,这与前面的示例非常不同:

function bar() {
    var foo;

    return foo;

    foo = function() {
    };
}

在这里,bar 返回 undefined,因为在 return 语句时,fooundefined,当然后面的赋值语句也没有执行。同样地,这是有效的且其行为是明确定义的:
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"

因为我们现在使用函数表达式,它们会按照逐步代码的方式出现,并且行为就像它看起来应该的那样。
将这一切带回到你的具体示例中:通常情况下,在循环中创建函数是一个不好的主意,但有时确实需要这样做。在这些场合,通常你需要一个像我的createHandler函数这样的辅助函数,它在循环之外,因此可以更好地控制上下文。你也可以这样做:
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));
}

...但这是一个非常糟糕的想法。首先,它很难阅读。其次,你会创建额外的函数:每个循环迭代实际上都会创建两个函数对象,一个用于创建另一个(例如,createHandler的函数),以及它创建的函数。因此,如果有三个对话框,你将创建六个函数而不是三个,并且所有这些函数都会一直存在,直到处理程序被移除。

最后需要注意的是:我上面展示的所有函数表达式都创建了匿名函数(没有名称的函数)。我不喜欢匿名函数;给函数命名有助于你的工具帮助你。从技术上讲,给它们命名是合法的:

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

...但目前在野外你不能这样做,因为IE9之前的IE存在错误。 IE8及以下版本将看到该结构两次,一次作为函数声明,然后再次作为函数表达式。它确实会创建两个函数对象,这可能会引起各种问题。 :-)


我认为你应该这样做:$(dialog_button).click((createHandler(dialog_button))()); 你需要在开始时执行该函数,以便它会给你一个新的可执行函数。 - Royi Namir
@RoyiNamir:我的代码是正确的。你的代码会调用createHandler去创建处理函数,然后立即执行返回的处理函数,并将其返回值传递到click中,这不是我们想要的。我们想要调用createHandler去创建函数对象并返回对它的引用(而不是调用它),然后将这个引用传递到click中,这样它就可以被设置为事件处理程序。当点击发生时,处理函数将被调用。 - T.J. Crowder
(确保它不在任何分支内,它必须在函数的顶层)-> 你能详细解释一下吗?我已经在for循环内声明了函数,它似乎可以工作!我做错了什么吗? - TDaver
@TDaver:在JavaScript中,函数声明和函数表达式之间有重要的区别,我会在答案中添加一条注释。 :-) - T.J. Crowder
+1,这样它就不会在底部腐烂了,但是希望你推荐forEach/jQuery.each,因为它们是最简单的解决方案 :-) - Esailija
@Esailija:你**1000%**正确。我真的,真的应该这样做,现在会回去这样做,因为这个答案已经被接受了。我看到Tadeck已经建议过了,我要去点赞。 - T.J. Crowder

6

闭包和 for 循环

是的,它正在执行不同的操作,这可以通过以下代码证明(以及这个 jsfiddle):

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

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

alert(test);

上述代码将提醒tests数组中的最后一个值,即5。这是由于for循环不是闭包,在它定义的变量也可以在外部访问。

jQuery.each()作为可能的解决方案

顺便说一下,jQuery有jQuery.each()帮助函数,可以帮助您遍历对象和数组,而无需使用for循环。在回调函数内部,局部变量将保持局部

解决方案

因此,基本上以下内容应该解决您的问题(但尚未经过测试,请先测试):
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');
    });
});

这篇文章对您有帮助吗?您是否有任何问题需要解决?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接