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
因爲綁定的回調函數是一個匿名函數,因此文中把形成這個現象的緣由歸結爲 這個函數是一個閉包,攜帶的做用域爲外層做用域,當事件觸發的時候,做用域中的變量已經隨着循環走到最後了。數組
注:閉包 = 函數 + 建立該函數的環境瀏覽器
我對此產生了不少疑問,若是說閉包是函數和建立時的環境,那麼事件綁定的時候(也就是這個匿名函數建立的時候),循環中的環境應該是循環當次,爲何直接到最後一次了呢?下面咱們就一步一步分析,到底是什麼緣由形成的。緩存
爲了搞懂這個問題,咱們從最簡單的循環開始閉包
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。
執行出來的結果是這樣的:
實驗發現,不管如何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次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
測試文檔