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() {
return alert("A");
}
}
else {
function foo() {
return alert("B");
}
}
return foo;
}
var f = bar(true);
f();
您可能认为警报会显示"A",对吗?因为我们为condition
传递了true
,所以第一个分支发生了。但实际上情况并非如此。从技术上讲,上面是语法错误,纯粹而简单。但大多数浏览器不会将其视为错误(Firefox的引擎[SpiderMonkey]是我知道的唯一一个)。那么他们会做什么?这取决于。大多数引擎继续将它们视为函数声明,当您在同一作用域中有两个相同函数的函数声明时,规范指定第二个函数获胜。因此,这些引擎将警报"B"。但其他引擎(IE就是其中之一)会即时重写您的代码,将这些声明转换为表达式,因此这些引擎将警报"A"。龙在这里,再见。不要这样做。 :-)
另一方面,函数表达式会将函数创建为逐步执行的代码的一部分。它们在执行流程到达它们时发生。因此,这与前面的示例非常不同:
function bar() {
var foo;
return foo;
foo = function() {
};
}
在这里,
bar
返回
undefined
,因为在
return
语句时,
foo
是
undefined
,当然后面的赋值语句也没有执行。同样地,这是有效的且其行为是明确定义的:
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();
f = bar(false);
f();
因为我们现在使用函数表达式,它们会按照逐步代码的方式出现,并且行为就像它看起来应该的那样。
将这一切带回到你的具体示例中:通常情况下,在循环中创建函数是一个不好的主意,但有时确实需要这样做。在这些场合,通常你需要一个像我的
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() {
};
...但目前在野外你不能这样做,因为IE9之前的IE存在错误。 IE8及以下版本将看到该结构两次,一次作为函数声明,然后再次作为函数表达式。它确实会创建两个函数对象,这可能会引起各种问题。 :-)
var foo = ...
实际上都是语义错误,因为实际上var foo
总是在函数开头,而foo = ...
则在代码中。 - user1046334