循环中的jQuery变量捕获

TDa*_*ver 2 javascript variables jquery

我对JS和jQuery的理解非常有限,我来自C#背景.但我确实知道变量捕获是什么,我知道如果我在循环中捕获一个声明为循环的OUTSIDE的变量,每次委托运行时我将得到捕获变量的最后一个值,而不是捕获的时间.但是,这显然不是问题,但我仍然收到最后一个值:

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;
        });
Run Code Online (Sandbox Code Playgroud)

}

由于dialog_button是在循环范围内声明的,我希望我会在click处理程序中收到正确的值.
JS做了哪些不同的事情?

T.J*_*der 9

JavaScript没有块范围,只有函数范围(以及全局范围).闭包接收变量的实时引用,因此事件处理函数(它是一个闭包)将始终看到分配给的最后一个值dialog_button.

在你描述的特定情况下你最好的选择是使用jQuery的$.each功能而不是for循环(谢谢你@Esailija,并注意到Tadeck $.each在我做之前的建议  - 值得一个upvote - 我在@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;
    });
});
Run Code Online (Sandbox Code Playgroud)

...因为现在,每次调用我们传递的函数都会$.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;
    };
}
Run Code Online (Sandbox Code Playgroud)

在那里,循环调用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;
        };
    }
}
Run Code Online (Sandbox Code Playgroud)

...或者如果您需要在其他地方做同样的事情,您可能会将其移出一个级别:

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;
    };
}
Run Code Online (Sandbox Code Playgroud)

后者的优点是它意味着存储器中的小块(称为变量绑定对象)由每个调用创建到createDialogs不被引用被任何东西,并且可以清除当调用返回,而与前(其中,createHandler是内createDialogs),该内存由调用的变量绑定对象引用createHandler,因此不符合清理条件.每个都有它的用途,它只取决于你是否需要访问createDialog调用上下文中的任何内容(你没有在你显示的代码中,但我意识到它只是一个摘录).

更多阅读:


你在评论中提到:

"(确保它不在任何分支内,它必须在函数的顶层)"你能详细说明吗?我在for循环中声明了这个函数,它似乎正在工作!难道我做错了什么?

JavaScript有两种不同的function结构:函数声明和函数表达式.在句法上它们非常相似,但它们在不同的地方是合法的,它们在不同的时间发生.

TL; DR:createHandler是函数声明的一个例子.它们不能在控制结构内.您function传入的构造click是一个函数表达式,可以是.区别在于function构造是否是右手值(我的不是,你的是).正确理解声明与表达式是熟练的JavaScript编程的关键.

长版:

这是一个函数声明:

function foo() {
}
Run Code Online (Sandbox Code Playgroud)

这是一个赋给变量的函数表达式:

var foo = function() {
};
Run Code Online (Sandbox Code Playgroud)

这是另一个函数表达式,这次用作对象文字中的属性初始值设定项:

var obj = {
    foo: function() {
         }
};
Run Code Online (Sandbox Code Playgroud)

另一个函数表达式,这次作为参数传递给函数:

bar(function() {
});
Run Code Online (Sandbox Code Playgroud)

如您所见,它们之间的区别在于函数声明是独立的,而函数表达式在包含表达式中用作右手值 - 作为赋值(=)或初始化函数(:)的右侧,或者作为参数传递给函数.

在执行任何分步代码之前,在创建包含它们的范围时处理函数声明.所以给出:

function bar() {

    function foo() {
    }

    return foo;
}
Run Code Online (Sandbox Code Playgroud)

...在bar调用时,在任何分步代码运行之前,将foo创建该函数.只有这样才能运行逐步代码,在这种情况下会返回对foo函数的引用.因此,上述内容与此完全相同:

function bar() {

    return foo;

    function foo() {
    }
}
Run Code Online (Sandbox Code Playgroud)

虽然它看起来foo不应该存在于return声明中,但确实如此.

由于函数声明发生在逐步代码之前,因此它们不能在控制流语句中:

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?
Run Code Online (Sandbox Code Playgroud)

你认为警报会说"A",对吗?因为我们通过truecondition,所以第一个分支发生了.但那根本不会发生什么.从技术上讲,上面是语法错误,纯粹而简单.但大多数浏览器并不把它当成一个(Firefox的引擎[SpiderMonkey]是我所知道的唯一一个).那么他们做了什么?这取决于.大多数引擎继续将它们视为函数声明,并且当在同一范围内对同一函数有两个函数声明时,规范说第二个获胜.所以这些引擎会提醒"B".但是其他引擎(IE就是其中之一)会动态地重写您的代码,将这些声明转换为表达式,因此这些引擎会提醒"A".在这里有Bye Dragons.不要这样做.:-)

另一方面,函数表达式创建函数作为逐步代码的一部分.它们在执行到达流程时发生.所以这与前面的例子非常不同:

function bar() {
    var foo;

    return foo;

    foo = function() {
    };
}
Run Code Online (Sandbox Code Playgroud)

在这里,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"
Run Code Online (Sandbox Code Playgroud)

因为现在我们正在使用函数表达式,所以它们出现在逐步代码中,并且行为就像它应该的那样.

将这一切带回到您的具体示例:一般来说,在循环中创建函数是一个坏主意,但有时候它是必要的.在这些情况下,通常你需要一个像我的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));
}
Run Code Online (Sandbox Code Playgroud)

......但这是一个非常糟糕的主意.首先,它很难阅读.其次,您创建了额外的函数:每个循环迭代实际上创建了两个函数对象,我们用来创建另一个函数对象(例如,那个createHandler),以及它创建的那个.因此,如果有三个对话框,则创建六个函数而不是三个,并且所有这些函数都会保留,直到删除处理程序.

最后一点:我上面显示的所有函数表达式都创建了匿名函数(没有名称的函数).我不喜欢匿名功能 ; 提供功能名称可以帮助您的工具.从技术上讲,给他们起名字是合法的:

var f = function foo() { // <== WARNING, see below
};
Run Code Online (Sandbox Code Playgroud)

...但是由于IE9之前的IE中错误,你现在无法在野外做到这一点.IE8及以下版本将看到该构造两次,一次作为函数声明,然后再作为函数表达式.它确实会创建两个函数对象,这可能会导致各种麻烦.:-)


Tad*_*eck 5

闭包和for循环

是的,它正在做一些不同的事情,这由以下(和这个jsfiddle)证明:

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

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

alert(test);
Run Code Online (Sandbox Code Playgroud)

以上将从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');
    });
});
Run Code Online (Sandbox Code Playgroud)

它有帮助吗?你有任何问题吗?