我正在阅读Mozilla开发人员关于闭包的网站,我在他们的示例中注意到常见错误,他们有这个代码:
<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
和
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}
}
并且他们说对于onFocus事件,代码只会显示最后一项的帮助,因为分配给onFocus事件的所有匿名函数都围绕'item'变量有一个闭包,这是有意义的,因为在JavaScript变量中没有块范围。解决方案是使用'let item = ...'代替,因为它具有块范围。
然而,我想知道为什么你不能在for循环之上声明'var item'?然后它具有setupHelp()的范围,并且每次迭代都会为它分配一个不同的值,然后将其作为闭包中的当前值捕获...对吗?
答案 0 :(得分:26)
因为在评估item.help
时,循环将完整地完成。相反,你可以用一个闭包来做到这一点:
for (var i = 0; i < helpText.length; i++) {
document.getElementById(helpText[i].id).onfocus = function(item) {
return function() {showHelp(item.help);};
}(helpText[i]);
}
JavaScript没有块范围,但确实有函数范围。通过创建闭包,我们将永久捕获对helpText[i]
的引用。
答案 1 :(得分:20)
闭包是一个函数和该函数的作用域环境。
在这种情况下,有助于理解Javascript如何实现范围。事实上,它只是一系列嵌套的词典。请考虑以下代码:
var global1 = "foo";
function myFunc() {
var x = 0;
global1 = "bar";
}
myFunc();
当程序开始运行时,你有一个范围字典,即全局字典,其中可能定义了许多内容:
{ global1: "foo", myFunc:<function code> }
假设你调用myFunc,它有一个局部变量x。为此函数的执行创建了一个新范围。函数的本地范围如下所示:
{ x: 0 }
它还包含对其父作用域的引用。所以函数的整个范围如下所示:
{ x: 0, parentScope: { global1: "foo", myFunc:<function code> } }
这允许myFunc修改global1。在Javascript中,每当您尝试为变量赋值时,它首先检查变量名称的本地范围。如果找不到,它会检查parentScope和该范围的parentScope等,直到找到该变量。
闭包实际上是一个函数加上一个指向该函数范围的指针(它包含指向其父作用域的指针,依此类推)。因此,在您的示例中,在for
循环执行完毕后,范围可能如下所示:
setupHelpScope = {
helpText:<...>,
i: 3,
item: {'id': 'age', 'help': 'Your age (you must be over 16)'},
parentScope: <...>
}
您创建的每个闭包都将指向此单个范围对象。如果我们要列出您创建的每个闭包,它将看起来像这样:
[anonymousFunction1, setupHelpScope]
[anonymousFunction2, setupHelpScope]
[anonymousFunction3, setupHelpScope]
当执行这些函数中的任何一个时,它使用传递的范围对象 - 在这种情况下,它与每个函数的范围对象相同!每个变量都会查看相同的item
变量并查看相同的值,这是您的for
循环设置的最后一个值。
要回答您的问题,是否在var item
循环上方或其中添加for
并不重要。由于for
循环不会创建自己的范围,因此item
将存储在当前函数的范围字典中,即setupHelpScope
。 for
循环内生成的附件始终指向setupHelpScope
。
一些重要的注释:
for
循环没有自己的作用域 - 它们只使用封闭函数的作用域。对于if
,while
,switch
等也是如此。如果这是C#,另一方面,将为每个循环创建一个新的范围对象,每个闭包都会包含指向其自身唯一范围的指针。anonymousFunction1
修改其范围内的变量,则会为其他匿名函数修改该变量。这可能会导致一些非常奇怪的互动。进一步阅读:
答案 2 :(得分:3)
我意识到最初的问题是五年了......但你也可以绑定一个不同的/特殊范围到你为每个元素分配的回调函数:
// Function only exists once in memory
function doOnFocus() {
// ...but you make the assumption that it'll be called with
// the right "this" (context)
var item = helpText[this.index];
showHelp(item.help);
};
for (var i = 0; i < helpText.length; i++) {
// Create the special context that the callback function
// will be called with. This context will have an attr "i"
// whose value is the current value of "i" in this loop in
// each iteration
var context = {index: i};
document.getElementById(helpText[i].id).onfocus = doOnFocus.bind(context);
}
如果你想要一个单行(或接近它):
// Kind of messy...
for (var i = 0; i < helpText.length; i++) {
document.getElementById(helpText[i].id).onfocus = function(){
showHelp(helpText[this.index].help);
}.bind({index: i});
}
或者更好的是,您可以使用EcmaScript 5.1的array.prototype.forEach
,它可以解决范围问题。
helpText.forEach(function(help){
document.getElementById(help.id).onfocus = function(){
showHelp(help);
};
});
答案 3 :(得分:2)
新作用域仅在function
块(和with
中创建,但不使用它)。像for
这样的循环不会创建新的范围。
因此,即使您在循环外声明了变量,也会遇到完全相同的问题。
答案 4 :(得分:1)
即使它在for循环之外声明,每个匿名函数仍将引用相同的变量,因此在循环之后,它们仍将指向item的最终值。