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