Javascriptclosures – variables范围的问题

我正在阅读closuresMozilla开发者网站,我注意到在他们的例子中常见的错误,他们有这样的代码:

<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”variables的闭包,这是有道理的,因为在JavaScriptvariables中没有块范围。 解决scheme是使用'let item = …'来代替,因为它具有块范围。

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

因为在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没有块范围,但是它具有函数范围。 通过创build闭包,我们永久地捕获对helpText[i]的引用。

闭包是该函数的一个函数和范围环境。

这有助于理解Javascript如何在这种情况下实现范围。 实际上,它只是一系列嵌套字典。 考虑这个代码:

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

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

 { global1: "foo", myFunc:<function code> } 

说你调用myFunc,它有一个局部variablesx。 为这个函数的执行创build一个新的作用域。 函数的本地作用域如下所示:

 { x: 0 } 

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

 { x: 0, parentScope: { global1: "foo", myFunc:<function code> } } 

这允许myFunc修改global1。 在Javascript中,每当您尝试为variables赋值时,它首先检查本地作用域的variables名称。 如果找不到,它将检查parentScope,以及该范围的parentScope等,直到find该variables。

闭包实际上是一个函数加上指向该函数作用域的指针(它包含一个指向其父作用域的指针,等等)。 所以,在你的例子中,在for循环完成执行之后,范围可能如下所示:

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

您创build的每个闭包将指向这个单一的作用域对象。 如果我们列出你创build的每一个闭包,它将看起来像这样:

 [anonymousFunction1, setupHelpScope] [anonymousFunction2, setupHelpScope] [anonymousFunction3, setupHelpScope] 

当这些函数中的任何一个函数执行时,它使用它传递的范围对象 – 在这种情况下,它是每个函数的相同范围对象! 每个人都会看到相同的itemvariables,并看到相同的值,这是您的for循环设置的最后一个值。

为了回答你的问题,无论你是在for循环上还是在里面添加var item ,都没关系。 因为for循环不会创build自己的作用域,所以item将被保存在当前函数的作用域字典中,即setupHelpScopefor循环中生成的shell将始终指向setupHelpScope

一些重要的说明:

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

进一步阅读:

  • Javascriptclosures – 如果您有兴趣,可以查看完整的细节。 那种心灵融化。
  • 克罗克福德模拟私人成员使用闭包 – 如果你能理解他是如何pipe理的,那么你现在就明白闭包了。
  • Crockford的页面上的Javascript – 一般的好东西

我意识到原来的问题是五年之久……但是你也可以一个不同的/特殊的作用域绑定到你赋给每个元素的callback函数:

 // 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); }; }); 

新的范围只能function块中创build( with ,但不要使用)。 循环喜欢不要创build新的范围。

所以,即使你在循环之外声明variables,你也会遇到同样的问题。

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