【问题标题】:Javascript closures - variable scope questionJavascript 闭包 - 变量范围问题
【发布时间】:2010-07-17 20:46:05
【问题描述】:

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

【问题讨论】:

  • javascript 有let?抬头……
  • @Kobi:它是 Mozilla 特定的 JavaScript 扩展。见this
  • 如果它是 Mozilla 特有的,那是否意味着我应该避免使用它?
  • 只有在您能保证您的用户永远不会使用非 Mozilla 浏览器的情况下。所以,实际上,没有。这是功能兼容性的有用参考:robertnyman.com/javascript/index.html

标签: javascript loops closures


【解决方案1】:

这是因为在评估 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] 的引用。

【讨论】:

  • 这是公认的标准解决方案,创建一个返回事件处理函数的函数,同时确定需要跟踪的变量的范围。有趣的旁注,jQuery 的 $.each() 自己完成此操作,因为它将索引和项目传递到您指定的函数中,从而使循环内部的范围变得容易。
【解决方案2】:

闭包是一个函数和该函数的作用域环境。

这有助于理解在这种情况下 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 循环设置的最后一个值。

要回答您的问题,无论是在 for 循环上方还是内部添加 var item 都没有关系。因为for 循环不会创建自己的作用域,所以item 将存储在当前函数的作用域字典中,即setupHelpScope。在for 循环内生成的附件将始终指向setupHelpScope

一些重要说明:

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

进一步阅读:

【讨论】:

  • 这是描述 JavaScript 作用域的好方法 - 但是您没有提供/解释问题的解决方案。要按原样保存变量的副本,您需要做的就是创建一个新作用域,方法是创建一个函数来返回一个函数:(function(item){ return function(){ ... } } )(item);,它可以访问作用域item。您还可以在代码的前面定义一个函数,例如:function genHelpFocus(item) { return function() { showHelp(item.html); } },以帮助防止膨胀范围泄漏。
  • 这是我听过的最清楚的解释。谢谢
【解决方案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);
   };
});

【讨论】:

    【解决方案4】:

    新作用域function 块(和with,但不要使用)中创建。像 for 这样的循环不会创建新的作用域。

    因此,即使您在循环之外声明变量,也会遇到完全相同的问题。

    【讨论】:

    • 请注意with 不会创建新的词法环境,例如,如果您在with 块内声明一个变量,该变量将被绑定到它的父范围(它将被提升)。 with 只是在作用域链前面引入了一个对象,more info
    【解决方案5】:

    即使在 for 循环之外声明,每个匿名函数仍然会引用同一个变量,所以在循环之后,它们仍然会指向 item 的最终值。

    【讨论】:

      猜你喜欢
      • 2011-04-13
      • 1970-01-01
      • 1970-01-01
      • 2013-02-17
      • 2017-07-18
      • 2010-11-08
      • 1970-01-01
      • 1970-01-01
      • 2014-01-13
      相关资源
      最近更新 更多