[Javascript實驗課]循環中的閉包

前言

https://developer.mozilla.org/zh-CN/docs/JavaScript/Guide/Closures
MDN上描述閉包的章節闡述了一個因爲閉包產生的常見錯誤,代碼片斷是這樣的html

for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }

簡言之就是循環中爲不一樣的元素綁定事件,事件回調函數裏若是調用了跟循環相關的變量,則這個變量取循環的最後一個值。segmentfault

因爲綁定的回調函數是一個匿名函數,因此文中把形成這個現象的緣由歸結爲 這個函數是一個閉包,攜帶的做用域爲外層做用域,當事件觸發的時候,做用域中的變量已經隨着循環走到最後了。數組

注:閉包 = 函數 + 建立該函數的環境瀏覽器

我對此產生了不少疑問,若是說閉包是函數和建立時的環境,那麼事件綁定的時候(也就是這個匿名函數建立的時候),循環中的環境應該是循環當次,爲何直接到最後一次了呢?下面咱們就一步一步分析,到底是什麼緣由形成的。緩存


簡單循環中的i

爲了搞懂這個問題,咱們從最簡單的循環開始閉包

for (var i = 0; i < 5; i++) {
     console.log(i)
}

毫無疑問,i會被逐次打印出來ide

for (var i = 0; i < 5; i++) {
    var a = function(){
        console.log(i)
    }
    a()
}

這裏,i也會被逐次打印出來,由於js裏,外層函數做用域會影響內層,而內層不會影響外層。基於這個原理,咱們也能夠加多少層都不要緊:函數

for (var i = 0; i < 5; i++) {
    var a = function(){
        return function(){
            console.log(i)
        }
    }
    a()()
}

每一層匿名函數和變量i都組成了一個閉包,可是這樣在循環中並無問題,由於函數在循環體中當即被執行了。setTimeout和事件則不太同樣,詳見下文。測試


setTimeout在循環裏

-setTimeout在循環中會怎樣呢?ui

for (var i = 0; i < 5; i++) {
    setTimeout(function(){
        console.log(i)
    },10)
}

不出所料,這裏果真出問題了,打印出來的結果爲5個5,遇到了前言中所述的因爲閉包所引發的常見錯誤。

根據內部可調用外部做用域的原理,setTimeout的回調函數裏面調用了外層的i,i和回調函數組成了閉包。i在循環執行以前是0,循環以後是5。

一切都瓜熟蒂落,很好理解,問題就是爲何setTimeout的回調不是每次取循環時的值,而取最後一次的值,難道setTimeout回調是在循環體外觸發的?

會不會是時間的問題,咱們把setTimeout的回調延遲設爲0毫秒試一下。

for (var i = 0; i < 5; i++) { 
    var a = function(){
        console.log(i)
    }
    setTimeout(a,0)
}

這並無解決問題

另注:其實setTimeout的延遲時間是存在最小值的,根據瀏覽器的不一樣有多是4ms 或者5ms,這意味着就算setTimeout設爲0,仍是有一小段的延遲的。
詳見:https://developer.mozilla.org/en-US/docs/Web/API/Window.setTimeout#Notes

爲了測試到底是不是時間的問題,我採用了下面這種更加殘暴的方式:

for (var i = 0; i < 100; i++) { 
    var a = function(){
        console.log(i)
    }
    a();
    setTimeout(a,0)
}

循環100次,一次普通調用,一次在setTimeout裏面調用,若是存在延遲,那麼setTimeout出來的結果會在一箇中間點,很難是100。

執行出來的結果是這樣的:

圖1

實驗發現,不管如何setTimeout都在最後執行,這證明了咱們以前遇到的問題,由於setTimeout在循環結束才執行,因此回調函數調用的i取值必然是循環的最後一次。

-setTimeout爲何會在最後執行呢,這是由於setTimeout的一種機制,setTimeout是從任務隊列結束的時候開始計時的,若是前面有進程沒有結束,那麼它就等到它結束再開始計時。在這裏,任務隊列就是它本身所在的循環。循環結束setTimeout纔開始計時,因此不管如何,setTimeout裏面的i都是最後一次循環的i。

解決辦法以下:

for (var i = 0; i < 5; i++) {
    var a = function(v){
        return function(){
            console.log(v)
        }
    }
    setTimeout(a(i),0)
}

不少人能利用上面的方法解決這個問題,由於setTimeout第一個參數須要一個函數,因此返回一個函數給它,返回的同時把i做爲參數傳進去,經過形參v緩存了i,並帶進返回的函數裏面。

下面這個方法則不行:

for (var i = 0; i < 5; i++) {
    var a = function(v){
        return function(){
            console.log(v)
        }
    }
    setTimeout(function(){
         a(i)
    },0)
}

這裏的問題是,回調函數沒有當即執行,自己又沒有傳入參數緩存

總結:例子中遇到setTimeout的問題,罪魁禍首是回調等待循環隊列結束形成的,解決的辦法是給回調函數傳一個實參緩存循環的數據。


循環中的事件

循環中的事件和setTimeout相似,也會涉及閉包問題,事件的listener,會和循環相關的變量造成一個閉包,在執行listener的時候,變量取最後一次循環的值。

for (var i = 0; i < 5; i++) { 
    var a = function(){
        console.log(i) 
    }
    document.body.addEventListener('click',a) 
}

可是和setTimeout不同的是,事件是須要觸發的,而絕大多數狀況下,觸發的時候循環已經結束了,因此循環相關的變量就是最後一次的取值,好比上例中,點擊body之後console 5次5,經過addEventListener添加的事件是能夠疊加的。

考慮下面的代碼:

for (var i = 0; i < 2; i++) { 
    var a = function(){
        console.log(i) 
    }
    document.body.addEventListener('click',a) 
}

for (var i = 0; i < 5; i++) { 
    var a = function(){
        console.log(i) 
    }
    document.body.addEventListener('click',a) 
}

答案是:
圖2

2次5和5次5,由於兩次循環使用了一樣的全局變量i,你點擊的時候這個i已經變成了5,無論事件是在兩次循環裏綁定的仍是五次循環裏綁定的,點擊回調只認全局變量i,跟在哪綁定的不要緊。

若是咱們想要2次2和5次5,就須要把前一次循環放到函數做用域裏或者把其中一個i換成別的變量名

(function(){
    for (var i = 0; i < 2; i++) { 
        var a = function(){
            console.log(i) 
        }
        document.body.addEventListener('click',a) 
    }

})()
for (var i = 0; i < 5; i++) { 
    var a = function(){
        console.log(i) 
    }
    document.body.addEventListener('click',a) 
}

至於解法,和setTimeout相似,也是經過listner形參緩存循環中的變量,如下代碼中,函數a返回一個函數,由於addeventlistner第二個參數接受的是函數,因此要這麼寫,而要執行的內容,寫在返回的這個函數體內。

for (var i = 0; i < 5; i++) { 
    var a = function(v){
        return function(){
            console.log(v)
        }
    }
    document.body.addEventListener('click',a(i))
}

總結

閉包並無那麼複雜,能夠簡單的理解爲函數體和外部做用域的一種關聯。

-setTimeout和綁定事件在循環常常會帶來意想不到的效果,取決於這兩個函數的特殊機制,閉包不是主因。

若是想在setTimeout和綁定事件保存住循環過程當中產生的變量,須要經過函數的實參傳進函數體。


參考(感謝如下做者):

http://www.cnblogs.com/hongdada/p/3359668.html

http://www.cnblogs.com/hh54188/p/3153358.html

https://developer.mozilla.org/en-US/docs/Web/API/EventTarget.addEventListener

https://developer.mozilla.org/en-US/docs/Web/API/Window.setTimeout

http://zh.wikipedia.org/wiki/%E9%97%AD%E5%8C%85_(%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6)

https://developer.mozilla.org/zh-CN/docs/JavaScript/Guide/Closures


測試文檔

http://jsfiddle.net/fishenal/wfU56/3/

相關文章
相關標籤/搜索