Javascript闭包 - 可变范围问题

Nic*_*ick 23 javascript closures loops

我在闭包上阅读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>  
Run Code Online (Sandbox Code Playgroud)

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

并且他们说对于onFocus事件,代码只会显示最后一项的帮助,因为分配给onFocus事件的所有匿名函数都有一个围绕'item'变量的闭包,这是有道理的,因为在JavaScript变量中没有块范围.解决方案是使用'let item = ...'代替,然后它有块范围.

但是,我想知道为什么你不能在for循环之上声明'var item'?然后它具有setupHelp()的范围,并且每次迭代都会为它分配一个不同的值,然后将其作为闭包中的当前值捕获...对吗?

whi*_*awk 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]);
}
Run Code Online (Sandbox Code Playgroud)

JavaScript没有块范围,但它具有函数范围.通过创建闭包,我们将helpText[i]永久捕获引用.

  • 这是标准接受的解决方案,创建一个返回事件处理函数的函数,同时还确定需要跟踪的变量.有趣的是,jQuery的[`$ .each()`](http://api.jquery.com/jQuery.each)自己做了这个,因为它将索引和项目传递给你指定的函数,因此,使循环内部的范围变得容易. (3认同)

End*_*der 20

闭包是函数和该函数的作用域环境.

在这种情况下,它有助于理解Javascript如何实现范围.事实上,它只是一系列嵌套的词典.考虑以下代码:

var global1 = "foo";

function myFunc() {
    var x = 0;
    global1 = "bar";
}

myFunc();
Run Code Online (Sandbox Code Playgroud)

当程序开始运行时,你有一个范围字典,即全局字典,它可能在其中定义了许多内容:

{ global1: "foo", myFunc:<function code> }
Run Code Online (Sandbox Code Playgroud)

假设你调用myFunc,它有一个局部变量x.为此函数的执行创建了一个新范围.函数的本地范围如下所示:

{ x: 0 }
Run Code Online (Sandbox Code Playgroud)

它还包含对其父作用域的引用.所以函数的整个范围如下所示:

{ x: 0, parentScope: { global1: "foo", myFunc:<function code> } }
Run Code Online (Sandbox Code Playgroud)

这允许myFunc修改global1.在Javascript中,每当您尝试为变量赋值时,它首先检查变量名称的本地范围.如果未找到,则检查parentScope和该范围的parentScope等,直到找到该变量.

闭包实际上是一个函数加上一个指向该函数范围的指针(它包含指向其父作用域的指针,依此类推).因此,在您的示例中,在for循环执行完毕后,范围可能如下所示:

setupHelpScope = {
  helpText:<...>,
  i: 3, 
  item: {'id': 'age', 'help': 'Your age (you must be over 16)'},
  parentScope: <...>
}
Run Code Online (Sandbox Code Playgroud)

您创建的每个闭包都将指向此单个范围对象.如果我们列出你创建的每个闭包,它看起来像这样:

[anonymousFunction1, setupHelpScope]
[anonymousFunction2, setupHelpScope]
[anonymousFunction3, setupHelpScope]
Run Code Online (Sandbox Code Playgroud)

当任何这些函数执行时,它使用传递的范围对象 - 在这种情况下,它是每个函数的相同范围对象!每个都将查看相同的item变量并查看相同的值,这是您的for循环设置的最后一个值.

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

一些重要的说明:

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

进一步阅读:

  • 这是描述JavaScript范围的好方法 - 但是您没有提供/解释问题的解决方案.要保存变量的副本,您需要做的就是创建一个新的范围,通过使函数返回一个函数:`(function(item){return function(){...}})( item);`有权访问作用域`item`.您还可以在代码中更早地定义函数,例如:`function genHelpFocus(item){return function(){showHelp(item.html); 有助于防止范围泄漏膨胀. (2认同)