for循环中let,var 的经典面试题:for循环中 console.log(i)详解
同學們在剛準備面試時肯定見過一道經典面試題:
for(var i = 0; i < 10; i++) {setTimeOut(function(){console.log(i)}) } // 輸出 10 10 10 10 10 10 10 10 10 10for(let i = 0; i < 10; i++) {setTimeOut(function(){console.log(i)}) } // 輸出 0 1 2 3 4 5 6 7 8 9有的同學就很疑惑,怎么肥事,這不一樣嘛!
然后就去百度查查
然后百度就會告訴你:
第一個變量i是用var聲明的,在全局范圍內有效,所以全局中只有一個變量i,每次循環時,setTimeOut定時器里指的是全局變量i,而循環里的十個setTimeOut是在循環結束后才執行,所以輸出十個10。
第二個變量i是用let聲明的,當前的i
只在本輪循環中有效,每次循環的i其實都是一個新的變量,所以setTImeOut定時器的里面的i其實不是同一變量,所以輸出0123456789
看完以后
似懂非懂
就算使用var定義的i是全局變量,每次循環都改變全局范圍里的i的值,但是循環一次,執行一次setTimeOut啊,不也應該輸出當前i值嗎?
em…
不管,面試官問我我就照著這么回答就行了,面試官肯定明白我說的什么
其實之前我理解的也是很片面的,直到這兩天看見一篇Google大佬的文章,全是英文,看了好半天****,下面👇,我就結合這篇文章給大家講講我的理解(瞎講講)吧
首先,在理解這個問題之前,我們需要理解一下macro-task(宏任務)和micro-task(微任務)
先給大家舉一個例子(大佬舉的例子):
console.log('script start');setTimeout(function() {console.log('setTimeout'); }, 0);Promise.resolve().then(function() {console.log('promise1'); }).then(function() {console.log('promise2'); })console.log('script end');大家來仔細猜一猜執行結果是什么吶?
結果: script start script end promise1 promise2 setTimeout嘿!有點東西
其實js是單線程的,每個線程有它自己的唯一的事件循環,但是事件循環的任務源可以不唯一。類似setTimeout, promise, ajax, DOM操作等都是典型的任務源,任務隊列中的任務便是來自這些任務源。而這些任務源產生的任務又可以分為macro-task(宏任務)和micro-task(微任務)兩種。
macro-task(宏任務)
macro-task(宏任務)中的任務都是有時間順序的,因此瀏覽器能夠有序地從中調度任務并執行。在任務與任務之間,瀏覽器可能會渲染更新。
macro-task(宏任務)中一個典型就是setTimeout,setTimeout函數等待給定的延遲事件然后將其回調函數推入宏任務Event Queue中。這就是為什么先輸出’script end’ 后輸出’setTimeout’的原因。
macro-task(宏任務)主要有:script(整體代碼), setTimeout, setInterval, setImmediate, I/O, UI rendering。
micro-task(微任務)
micro-task(微任務)中的任務在當前函數調用棧中的函數執行完成之后即調度,像promise、mutation都會被推入微任務Event Queue隊列中。并且微任務Event Queue隊列中的一個任務執行完成后,后續的micro-task(微任務)也會繼續執行,直到微任務Event Queue隊列為空,這就解釋了為什么promise2也會在setTimeout之前輸出的原因。
微任務Event Queue隊列主要有process.nextTick, Promise, Object.observe(已廢棄), MutationObserver(html5新特性)
當宏任務Event Queue隊列中的一個任務執行結束時,如果函數調用棧為空,便會開始執行微任務Event Queue隊列中的任務,直至微任務Event Queue隊列中所有任務執行完畢,然后event loop才會繼續執行宏任務Event Queue隊列中的下一個任務。
上圖文:
接下來,上個厲害的(也是大佬舉的例子)
<div class="outer"><div class="inner"></div> </div>分別給這兩個div加上點擊事件
var outer = document.querySelector('.outer'); var inner = document.querySelector('.inner');new MutationObserver(function () {console.log('mutate'); }).observe(outer, {attributes: true, });function onClick() {console.log('click');setTimeout(function () {console.log('timeout');}, 0);Promise.resolve().then(function () {console.log('promise');});outer.setAttribute('data-random', Math.random()); }inner.addEventListener('click', onClick); outer.addEventListener('click', onClick);大家再來仔細思考一下執行結果是什么吶?
結果: click promise mutate click promise mutate timeout timeout
嘿!有點大東西
上述例子中Dispatch click和setTimeout屬于宏任務Event Queue,對應的回調函數被推進宏任務Event Queue隊列中,Mutation observer和promise屬于微任務Event Queue,對應的回調函數則被推入微任務Event Queue隊列中。當點擊inner元素時,代碼執行執行過程如下所示
Dispatch click被推入宏任務Event Queue隊列中,當點擊inner元素時onClick被推入函數調用棧(Js stack)中,執行上下文進入onClick中,將setTimeout的回調函數推入宏任務Event Queue隊列中,Mutation observers和Promise then的回調函數推入微任務Event Queue隊列中,并執行輸出click。
當onClick執行結束后, 函數調用棧為空,將微任務Event Queue隊列中的 promise then 的回調函數推入函數調用棧。
同樣地,當promise then的回調函數執行結束后,將mutation observers的回調函數推入函數調用棧
由于事件冒泡機制,父元素outer也會響應點擊事件,因此重復1-3步驟,執行結束后如下所示。
此時函數調用棧和microtasks中均為空,因此event loop將執行tasks中的下一個任務。
再執行tasks中的最后一個任務。
(注:以上例子均在谷歌瀏覽器中的輸出結果)
上圖文:
把這兩個例子理解好,是不是感覺對 這道經典面試題 豁然開朗,甚至有點小菜一碟了吶、
其實最后總結一下就是:
當瀏覽器在執行這段
代碼時
先在全局定義變量 i, 然后執行 for 循環,執行一次 for 循環,分別將 i++ 放入函數調用棧隊列,setTimeout 放入task隊列 一次。
因為需要將函數調用棧隊列里的任務執行結束后,再往下執行task任務
所以 i++ 一直在執行,10次 i++ 執行結束, i 的值為10(為什么不是9?因為 i++ 在值為9時,還會進行一次i++操作,最后一個循環完 i 的值為10,不滿足條件,不再循環)。
至此,函數調用棧隊列任務執行結束,再去執行task里的十個setTimeout任務,2而此時 i 的值為10,所以輸出10 個 10。
最后 附上 :Google 大佬文章
感謝大佬!雖然我們只是大佬的搬運工,但是希望在搬運之前理解好我們所搬運的內容,這已經很厲害啦。
以上理解若有偏差,望各位同學們批評指正。
總結
以上是生活随笔為你收集整理的for循环中let,var 的经典面试题:for循环中 console.log(i)详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Optional 处理空指针
- 下一篇: 企业实战(Jenkins+GitLab+