Javascript闭包 - 可变范围问题

时间:2010-07-17 20:46:06

标签: javascript loops closures

我正在阅读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()的范围,并且每次迭代都会为它分配一个不同的值,然后将其作为闭包中的当前值捕获...对吗?

5 个答案:

答案 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将存储在当前函数的范围字典中,即setupHelpScopefor循环内生成的附件始终指向setupHelpScope

一些重要的注释:

  • 出现这种情况是因为,在Javascript中,for循环没有自己的作用域 - 它们只使用封闭函数的作用域。对于ifwhileswitch等也是如此。如果这是C#,另一方面,将为每个循环创建一个新的范围对象,每个闭包都会包含指向其自身唯一范围的指针。
  • 请注意,如果anonymousFunction1修改其范围内的变量,则会为其他匿名函数修改该变量。这可能会导致一些非常奇怪的互动。
  • 范围只是对象,就像您编程的对象一样。具体来说,他们是字典。 JS虚拟机就像使用垃圾收集器一样,从内存中管理它们的删除。出于这个原因,过度使用闭包可能会造成真正的内存膨胀。由于闭包包含一个指向作用域对象的指针(该对象又包含指向其父作用域对象的指针),因此整个作用域链不能被垃圾收集,并且必须在内存中保留。

进一步阅读:

答案 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的最终值。