2010-07-17 25 views
23

我讀上關閉Mozilla的開發者的網站,我在他們的常見錯誤例如注意到,他們有這樣的代碼:的Javascript關閉 - 變量的作用域問題

<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()的範圍,並且每次迭代都會爲其分配一個不同的值,然後將其作爲當前值在閉包中捕獲......對吧?

+2

javascript has'let'?查找... – Kobi 2010-07-17 20:50:30

+1

@Kobi:這是一個Mozilla特定的JavaScript擴展。見[this](https://developer.mozilla.org/en/new_in_javascript_1.7)。 – 2010-07-17 21:06:52

+0

如果它是Mozilla特有的,這是否意味着我應該避免使用它? – Nick 2010-07-17 22:45:59

回答

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]的引用。

+3

這是標準的接受解決方案,使一個函數返回你的事件處理函數,同時還確定變量的範圍需要跟蹤。有趣的一面是,jQuery的['.each()'](http://api.jquery.com/jQuery.each)可以自己完成這個工作,因爲它將索引和項目傳遞給你指定的函數,從而使循環內部的範圍變得容易。 – gnarf 2010-07-17 20:54:49

1

即使它是在for循環之外聲明的,每個匿名函數仍然會引用相同的變量,所以在循環之後,它們仍然會指向item的最終值。

2

新的示波器只有創建function塊(和with,但不要使用它)。像for這樣的循環不會創建新的範圍。

因此,即使您在循環外聲明變量,也會遇到完全相同的問題。

+2

只要注意'with'不會創建一個新的詞法環境,例如,如果*在'with'塊中聲明瞭一個變量,則該變量將被綁定到它的父範圍(它將被*提升*) 。 'with'只是在作用域鏈前面引入一個對象,[更多信息](http://stackoverflow.com/questions/2742819/) – CMS 2010-07-17 22:08:21

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循環設置的最後一個值。

要回答你的問題,無論你在for循環之上還是在裏面添加var item都沒有關係。因爲for循環不會創建自己的作用域,所以item將存儲在當前函數的作用域字典中,即setupHelpScope。在for循環內生成的外殼將始終指向setupHelpScope

一些重要注意事項:

  • 出現此現象的原因,在Javascript中,for循環沒有自己的範圍 - 他們僅僅使用了封閉函數的範圍。這同樣適用於if,while,switch等。如果這是C#,另一方面,將爲每個循環創建一個新的作用域對象,並且每個閉包將包含一個指向其自己唯一作用域的指針。
  • 請注意,如果anonymousFunction1修改其範圍內的變量,它將修改其他匿名函數的變量。這可能會導致一些非常奇怪的交互。
  • 範圍只是對象,就像您編程的對象一樣。具體來說,他們是字典。 JS虛擬機與其他任何東西一樣管理從內存中刪除的內容 - 使用垃圾收集器。出於這個原因,過度使用閉包可能會造成真正的內存膨脹。由於閉包包含一個指向作用域對象的指針(它又包含一個指向其父作用域對象的指針),整個作用域鏈不能被垃圾收集,而必須在內存中保留。

延伸閱讀:

+2

這是描述JavaScript範圍的好方法 - 但是你沒有提供/解釋問題的解決方案。爲了保存變量的副本,你需要做的就是創建一個新的範圍,通過創建一個函數來返回一個函數:'(function(item){return function(){...}})( item);'有權訪問scoped'item'。你也可以在代碼中定義一個函數,例如:'function genHelpFocus(item){return function(){showHelp(item.html); }}'以幫助防止擴大範圍泄漏。 – gnarf 2010-07-18 00:44:18

+0

這是我聽到的最清晰的解釋。謝謝 – Adam 2015-12-03 12:13:56

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