摘要:執行出來的結果是這樣的實驗發現,無論如何都在最后執行,這證實了我們之前遇到的問題,因為在循環結束才執行,所以回調函數調用的取值必然是循環的最后一次。
前言
https://developer.mozilla.org/zh-CN/docs/JavaScript/Guide/Closures
MDN上描述閉包的章節闡述了一個由于閉包產生的常見錯誤,代碼片段是這樣的
for (var i = 0; i < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); } }
簡言之就是循環中為不同的元素綁定事件,事件回調函數里如果調用了跟循環相關的變量,則這個變量取循環的最后一個值。
由于綁定的回調函數是一個匿名函數,所以文中把造成這個現象的原因歸結為 這個函數是一個閉包,攜帶的作用域為外層作用域,當事件觸發的時候,作用域中的變量已經隨著循環走到最后了。
注:閉包 = 函數 + 創建該函數的環境
我對此產生了很多疑問,如果說閉包是函數和創建時的環境,那么事件綁定的時候(也就是這個匿名函數創建的時候),循環中的環境應該是循環當次,為什么直接到最后一次了呢?下面我們就一步一步分析,究竟是什么原因造成的。
簡單循環中的i為了搞懂這個問題,我們從最簡單的循環開始
for (var i = 0; i < 5; i++) { console.log(i) }
毫無疑問,i會被逐次打印出來
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在循環中會怎樣呢?
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
測試文檔
http://jsfiddle.net/fishenal/wfU56/3/
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/92316.html
摘要:在第一次循環的時候并沒有被賦值,所以是,在第二次循環的時候,定時器其實清理的是上一個循環的定時器。所以導致每次循環都是清理上一次的定時器,而最后一次循環的定時器沒被清理,導致一直輸出。 Javascript Evet Loop 模型 setTimeout()最短的事件間隔是4mssetInterval()最短的事件間隔是10ms以上這個理論反正我是沒有驗證過 Exemple 1 --...
摘要:前言最近參加了幾場面試,積累了一些高頻面試題,我把面試題分為兩類,一種是基礎試題主要考察前端技基礎是否扎實,是否能夠將前端知識體系串聯。 前言 最近參加了幾場面試,積累了一些高頻面試題,我把面試題分為兩類,一種是基礎試題: 主要考察前端技基礎是否扎實,是否能夠將前端知識體系串聯。一種是開放式問題: 考察業務積累,是否有自己的思考,思考問題的方式,這類問題沒有標準答案。 基礎題 題目的答...
摘要:權威指南第版中閉包的定義函數對象可以通過作用域鏈相互關聯起來,函數體內部的變量都可以保存在函數作用域內,這種特性在計算機科學文獻中成為閉包。循環中的閉包使用閉包時一種常見的錯誤情況是循環中的閉包,很多初學者都遇到了這個問題。 閉包簡介 閉包是JavaScript的重要特性,那么什么是閉包? 《JavaScript高級程序設計(第3版)》中閉包的定義: 閉包就是指有權訪問另一個函數中的變...
摘要:局部變量,當定義該變量的函數調用結束時,該變量就會被垃圾回收機制回收而銷毀。如果在函數中不使用匿名函數創建閉包,而是通過引用一個外部函數,也不會出現循環引用的問題。 閉包是什么 在 JavaScript 中,閉包是一個讓人很難弄懂的概念。ECMAScript 中給閉包的定義是:閉包,指的是詞法表示包括不被計算的變量的函數,也就是說,函數可以使用函數之外定義的變量。 是不是看完這個定義感...
摘要:同步異步回調傻傻分不清楚。分割線上面主要講了同步和回調執行順序的問題,接著我就舉一個包含同步異步回調的例子。同步優先回調內部有個,第二個是一個回調回調墊底。異步也,輪到回調的孩子們回調,出來執行了。 同步、異步、回調?傻傻分不清楚。 大家注意了,教大家一道口訣: 同步優先、異步靠邊、回調墊底(讀起來不順) 用公式表達就是: 同步 => 異步 => 回調 這口訣有什么用呢?用來對付面試的...
閱讀 2014·2021-08-21 14:09
閱讀 482·2019-08-30 15:44
閱讀 2107·2019-08-29 16:32
閱讀 1371·2019-08-29 15:36
閱讀 3433·2019-08-29 12:43
閱讀 2776·2019-08-29 11:14
閱讀 430·2019-08-28 18:26
閱讀 2246·2019-08-26 13:57