摘要:執(zhí)行出來的結(jié)果是這樣的實驗發(fā)現(xiàn),無論如何都在最后執(zhí)行,這證實了我們之前遇到的問題,因為在循環(huán)結(jié)束才執(zhí)行,所以回調(diào)函數(shù)調(diào)用的取值必然是循環(huán)的最后一次。
前言
https://developer.mozilla.org/zh-CN/docs/JavaScript/Guide/Closures
MDN上描述閉包的章節(jié)闡述了一個由于閉包產(chǎn)生的常見錯誤,代碼片段是這樣的
for (var i = 0; i < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); } }
簡言之就是循環(huán)中為不同的元素綁定事件,事件回調(diào)函數(shù)里如果調(diào)用了跟循環(huán)相關(guān)的變量,則這個變量取循環(huán)的最后一個值。
由于綁定的回調(diào)函數(shù)是一個匿名函數(shù),所以文中把造成這個現(xiàn)象的原因歸結(jié)為 這個函數(shù)是一個閉包,攜帶的作用域為外層作用域,當事件觸發(fā)的時候,作用域中的變量已經(jīng)隨著循環(huán)走到最后了。
注:閉包 = 函數(shù) + 創(chuàng)建該函數(shù)的環(huán)境
我對此產(chǎn)生了很多疑問,如果說閉包是函數(shù)和創(chuàng)建時的環(huán)境,那么事件綁定的時候(也就是這個匿名函數(shù)創(chuàng)建的時候),循環(huán)中的環(huán)境應(yīng)該是循環(huán)當次,為什么直接到最后一次了呢?下面我們就一步一步分析,究竟是什么原因造成的。
簡單循環(huán)中的i為了搞懂這個問題,我們從最簡單的循環(huán)開始
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里,外層函數(shù)作用域會影響內(nèi)層,而內(nèi)層不會影響外層?;谶@個原理,我們也可以加多少層都沒關(guān)系:
for (var i = 0; i < 5; i++) { var a = function(){ return function(){ console.log(i) } } a()() }
每一層匿名函數(shù)和變量i都組成了一個閉包,但是這樣在循環(huán)中并沒有問題,因為函數(shù)在循環(huán)體中立即被執(zhí)行了。setTimeout和事件則不太一樣,詳見下文。
setTimeout在循環(huán)里-setTimeout在循環(huán)中會怎樣呢?
for (var i = 0; i < 5; i++) { setTimeout(function(){ console.log(i) },10) }
不出所料,這里果然出問題了,打印出來的結(jié)果為5個5,遇到了前言中所述的由于閉包所引起的常見錯誤。
根據(jù)內(nèi)部可調(diào)用外部作用域的原理,setTimeout的回調(diào)函數(shù)里面調(diào)用了外層的i,i和回調(diào)函數(shù)組成了閉包。i在循環(huán)執(zhí)行之前是0,循環(huán)之后是5。
一切都順理成章,很好理解,問題就是為什么setTimeout的回調(diào)不是每次取循環(huán)時的值,而取最后一次的值,難道setTimeout回調(diào)是在循環(huán)體外觸發(fā)的?
會不會是時間的問題,我們把setTimeout的回調(diào)延遲設(shè)為0毫秒試一下。
for (var i = 0; i < 5; i++) { var a = function(){ console.log(i) } setTimeout(a,0) }
這并沒有解決問題
另注:其實setTimeout的延遲時間是存在最小值的,根據(jù)瀏覽器的不同有可能是4ms 或者5ms,這意味著就算setTimeout設(shè)為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) }
循環(huán)100次,一次普通調(diào)用,一次在setTimeout里面調(diào)用,如果存在延遲,那么setTimeout出來的結(jié)果會在一個中間點,很難是100。
執(zhí)行出來的結(jié)果是這樣的:
實驗發(fā)現(xiàn),無論如何setTimeout都在最后執(zhí)行,這證實了我們之前遇到的問題,因為setTimeout在循環(huán)結(jié)束才執(zhí)行,所以回調(diào)函數(shù)調(diào)用的i取值必然是循環(huán)的最后一次。
-setTimeout為什么會在最后執(zhí)行呢,這是因為setTimeout的一種機制,setTimeout是從任務(wù)隊列結(jié)束的時候開始計時的,如果前面有進程沒有結(jié)束,那么它就等到它結(jié)束再開始計時。在這里,任務(wù)隊列就是它自己所在的循環(huán)。循環(huán)結(jié)束setTimeout才開始計時,所以無論如何,setTimeout里面的i都是最后一次循環(huán)的i。
解決辦法如下:
for (var i = 0; i < 5; i++) { var a = function(v){ return function(){ console.log(v) } } setTimeout(a(i),0) }
很多人能利用上面的方法解決這個問題,因為setTimeout第一個參數(shù)需要一個函數(shù),所以返回一個函數(shù)給它,返回的同時把i作為參數(shù)傳進去,通過形參v緩存了i,并帶進返回的函數(shù)里面。
下面這個方法則不行:
for (var i = 0; i < 5; i++) { var a = function(v){ return function(){ console.log(v) } } setTimeout(function(){ a(i) },0) }
這里的問題是,回調(diào)函數(shù)沒有立即執(zhí)行,本身又沒有傳入?yún)?shù)緩存。
總結(jié):例子中遇到setTimeout的問題,罪魁禍首是回調(diào)等待循環(huán)隊列結(jié)束造成的,解決的辦法是給回調(diào)函數(shù)傳一個實參緩存循環(huán)的數(shù)據(jù)。
循環(huán)中的事件循環(huán)中的事件和setTimeout類似,也會涉及閉包問題,事件的listener,會和循環(huán)相關(guān)的變量形成一個閉包,在執(zhí)行l(wèi)istener的時候,變量取最后一次循環(huán)的值。
for (var i = 0; i < 5; i++) { var a = function(){ console.log(i) } document.body.addEventListener("click",a) }
但是和setTimeout不一樣的是,事件是需要觸發(fā)的,而絕大多數(shù)情況下,觸發(fā)的時候循環(huán)已經(jīng)結(jié)束了,所以循環(huán)相關(guān)的變量就是最后一次的取值,比如上例中,點擊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,因為兩次循環(huán)使用了同樣的全局變量i,你點擊的時候這個i已經(jīng)變成了5,不管事件是在兩次循環(huán)里綁定的還是五次循環(huán)里綁定的,點擊回調(diào)只認全局變量i,跟在哪綁定的沒關(guān)系。
如果我們想要2次2和5次5,就需要把前一次循環(huán)放到函數(shù)作用域里或者把其中一個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形參緩存循環(huán)中的變量,以下代碼中,函數(shù)a返回一個函數(shù),因為addeventlistner第二個參數(shù)接受的是函數(shù),所以要這么寫,而要執(zhí)行的內(nèi)容,寫在返回的這個函數(shù)體內(nèi)。
for (var i = 0; i < 5; i++) { var a = function(v){ return function(){ console.log(v) } } document.body.addEventListener("click",a(i)) }總結(jié)
閉包并沒有那么復(fù)雜,可以簡單的理解為函數(shù)體和外部作用域的一種關(guān)聯(lián)。
-setTimeout和綁定事件在循環(huán)經(jīng)常會帶來意想不到的效果,取決于這兩個函數(shù)的特殊機制,閉包不是主因。
如果想在setTimeout和綁定事件保存住循環(huán)過程中產(chǎn)生的變量,需要通過函數(shù)的實參傳進函數(shù)體。
參考(感謝以下作者):
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/
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.hztianpu.com/yun/92316.html
摘要:在第一次循環(huán)的時候并沒有被賦值,所以是,在第二次循環(huán)的時候,定時器其實清理的是上一個循環(huán)的定時器。所以導(dǎo)致每次循環(huán)都是清理上一次的定時器,而最后一次循環(huán)的定時器沒被清理,導(dǎo)致一直輸出。 Javascript Evet Loop 模型 setTimeout()最短的事件間隔是4mssetInterval()最短的事件間隔是10ms以上這個理論反正我是沒有驗證過 Exemple 1 --...
摘要:前言最近參加了幾場面試,積累了一些高頻面試題,我把面試題分為兩類,一種是基礎(chǔ)試題主要考察前端技基礎(chǔ)是否扎實,是否能夠?qū)⑶岸酥R體系串聯(lián)。 前言 最近參加了幾場面試,積累了一些高頻面試題,我把面試題分為兩類,一種是基礎(chǔ)試題: 主要考察前端技基礎(chǔ)是否扎實,是否能夠?qū)⑶岸酥R體系串聯(lián)。一種是開放式問題: 考察業(yè)務(wù)積累,是否有自己的思考,思考問題的方式,這類問題沒有標準答案。 基礎(chǔ)題 題目的答...
摘要:權(quán)威指南第版中閉包的定義函數(shù)對象可以通過作用域鏈相互關(guān)聯(lián)起來,函數(shù)體內(nèi)部的變量都可以保存在函數(shù)作用域內(nèi),這種特性在計算機科學(xué)文獻中成為閉包。循環(huán)中的閉包使用閉包時一種常見的錯誤情況是循環(huán)中的閉包,很多初學(xué)者都遇到了這個問題。 閉包簡介 閉包是JavaScript的重要特性,那么什么是閉包? 《JavaScript高級程序設(shè)計(第3版)》中閉包的定義: 閉包就是指有權(quán)訪問另一個函數(shù)中的變...
摘要:局部變量,當定義該變量的函數(shù)調(diào)用結(jié)束時,該變量就會被垃圾回收機制回收而銷毀。如果在函數(shù)中不使用匿名函數(shù)創(chuàng)建閉包,而是通過引用一個外部函數(shù),也不會出現(xiàn)循環(huán)引用的問題。 閉包是什么 在 JavaScript 中,閉包是一個讓人很難弄懂的概念。ECMAScript 中給閉包的定義是:閉包,指的是詞法表示包括不被計算的變量的函數(shù),也就是說,函數(shù)可以使用函數(shù)之外定義的變量。 是不是看完這個定義感...
摘要:同步異步回調(diào)傻傻分不清楚。分割線上面主要講了同步和回調(diào)執(zhí)行順序的問題,接著我就舉一個包含同步異步回調(diào)的例子。同步優(yōu)先回調(diào)內(nèi)部有個,第二個是一個回調(diào)回調(diào)墊底。異步也,輪到回調(diào)的孩子們回調(diào),出來執(zhí)行了。 同步、異步、回調(diào)?傻傻分不清楚。 大家注意了,教大家一道口訣: 同步優(yōu)先、異步靠邊、回調(diào)墊底(讀起來不順) 用公式表達就是: 同步 => 異步 => 回調(diào) 這口訣有什么用呢?用來對付面試的...
閱讀 2076·2021-08-21 14:09
閱讀 540·2019-08-30 15:44
閱讀 2178·2019-08-29 16:32
閱讀 1440·2019-08-29 15:36
閱讀 3562·2019-08-29 12:43
閱讀 2835·2019-08-29 11:14
閱讀 486·2019-08-28 18:26
閱讀 2302·2019-08-26 13:57