沒有錯,這道題就是:javascript
for (var i = 0; i< 10; i++){ setTimeout(() => { console.log(i); }, 1000) } // 10 10 10 10 ...
爲何這裏會出現10次10,而不是咱們預期的0-9呢?咱們要如何修改達到預期效果呢?java
首先咱們得理解setTimeout
中函數的執行時機,這裏就要講到一個運行時的概念。es6
函數調用造成了一個棧幀。數組
function foo(b) { var a = 10; return a + b + 11; } function bar(x) { var y = 3; return foo(x * y); } console.log(bar(7)); // 返回 42
當調用 bar
時,建立了第一個幀 ,幀中包含了 bar
的參數和局部變量。當 bar
調用 foo
時,第二個幀就被建立,並被壓到第一個幀之上,幀中包含了 foo
的參數和局部變量。當 foo
返回時,最上層的幀就被彈出棧(剩下 bar
函數的調用幀 )。當 bar
返回的時候,棧就空了。promise
對象被分配在一個堆中,即用以表示一大塊非結構化的內存區域。併發
一個 JavaScript 運行時包含了一個待處理的消息隊列。每個消息都關聯着一個用以處理這個消息的函數。異步
在事件循環(Event Loop)期間的某個時刻,運行時從最早進入隊列的消息開始處理隊列中的消息。爲此,這個消息會被移出隊列,並做爲輸入參數調用與之關聯的函數。正如前面所提到的,調用一個函數老是會爲其創造一個新的棧幀。async
函數的處理會一直進行到執行棧再次爲空爲止;而後事件循環將會處理隊列中的下一個消息(若是還有的話)。函數
這裏setTimeout
會等到當前隊列執行完了以後再執行,即for
循環結束後執行,而這個時候i
的值已是10
了,因此會打印出來10個10這樣的結果。oop
要是想獲得預期效果,簡單的刪除setTimeout
也是可行的。固然也能夠這樣改setTimeout(console.log, 1000, i);
將i
做爲參數傳入函數。
仔細查閱規範可知,異步任務可分爲 task
和 microtask
兩類,不一樣的API註冊的異步任務會依次進入自身對應的隊列中,而後等待 Event Loop 將它們依次壓入執行棧中執行。
(macro)task主要包含:script(總體代碼)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js 環境)
microtask主要包含:Promise.then、MutaionObserver、process.nextTick(Node.js 環境)
附上一幅圖更清楚的瞭解一下
每一次Event Loop觸發時:
其實promise的then和catch纔是microtask,自己的內部代碼不是。
new Promise(resolve => { resolve(1); Promise.resolve().then(() => console.log(2)); console.log(4) }).then(t => console.log(t)); console.log(3);
這道題比較基礎,答案爲4321。先執行同步任務,打印出43,而後分析微任務,2先入任務隊列先執行,再打印出1。
這裏還有幾種變種,結果相似。
let promise1 = new Promise(resolve => { resolve(2); }); new Promise(resolve => { resolve(1); Promise.resolve(2).then(v => console.log(v)); //Promise.resolve(Promise.resolve(2)).then(v => console.log(v)); //Promise.resolve(promise1).then(v => console.log(v)); //new Promise(resolve=>{resolve(2)}).then(v => console.log(v)); console.log(4) }).then(t => console.log(t)); console.log(3);
不過要值得注意的是一下兩種狀況:
let thenable = { then: function(resolve, reject) { resolve(2); } }; new Promise(resolve => { resolve(1); new Promise(resolve => { resolve(promise1); }).then(v => { console.log(v); }); // Promise.resolve(thenable).then(v => { // console.log(v); // }); console.log(4); }).then(t => console.log(t)); console.log(3);
let promise1 = new Promise(resolve => { resolve(thenable); }); new Promise(resolve => { resolve(1); Promise.resolve(promise1).then(v => { console.log(v); }); // new Promise(resolve => { // resolve(promise1); // }).then(v => { // console.log(v); // }); console.log(4); }).then(t => console.log(t)); console.log(3);
結果爲4312。有人可能會說阮老師這篇文章裏提過
Promise.resolve('foo') // 等價於 new Promise(resolve => resolve('foo'))
那爲何這兩個的結果不同呢?
請注意這裏resolve
的前提條件是參數是一個原始值,或者是一個不具備then
方法的對象,而其餘狀況是怎樣的呢,stackoverflow上這個問題分析的比較透徹,我這裏簡單的總結一下。
這裏的RESOLVE('xxx')是new Promise(resolve=>resolve('xxx'))簡寫
Promise.resolve('nonThenable')
和 RESOLVE('nonThenable')
相似;Promise.resolve(thenable)
和 RESOLVE(thenable)
相似;Promise.resolve(promise)
要根據promise
對象的resolve
來區分,不爲thenable
的話狀況和Promise.resolve('nonThenable')
類似;RESOLVE(thenable)
和 RESOLVE(promise)
能夠理解爲 new Promise((resolve, reject) => { Promise.resolve().then(() => { thenable.then(resolve) }) })
也就是說能夠理解爲Promise.resolve(thenable)
會在這一次的Event Loop
中當即執行thenable
對象的then
方法,而後將外部的then
調入下一次循環中執行。
再形象一點理解,能夠理解爲RESOLVE(thenable).then
和PROMISE.then.then
的語法相似。
再來一道略微複雜一點的題加深印象
async function async1() { console.log('async1 start'); await async2(); console.log('async1 end'); } async function async2() { console.log('async2'); } console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0) async1(); new Promise(function(resolve) { console.log('promise1'); resolve(); }).then(function() { console.log('promise2'); }); console.log('script end');
答案
/* script start async1 start async2 promise1 script end async1 end promise2 setTimeout */
總結一下阮老師的介紹。
let
命令,用來聲明變量。它的用法相似於var
,可是所聲明的變量,只在let
命令所在的代碼塊內有效。而var
全局有效。var
命令會發生「變量提高」現象,即變量能夠在聲明以前使用,值爲undefined
。let
命令改變了語法行爲,它所聲明的變量必定要在聲明後使用,不然報錯。let
命令聲明變量以前,該變量都是不可用的。這在語法上,稱爲「暫時性死區」(temporal dead zone,簡稱 TDZ)。let
不容許在相同做用域內,重複聲明同一個變量。const
聲明的變量不得改變值,這意味着,const
一旦聲明變量,就必須當即初始化,不能留到之後賦值。const
其餘用法和let
相同。上面代碼中,變量i
是var
命令聲明的,在全局範圍內都有效,因此全局只有一個變量i
。每一次循環,變量i
的值都會發生改變,而循環內被賦給數組a
的函數內部的console.log(i)
,裏面的i
指向的就是全局的i
。也就是說,全部數組a
的成員裏面的i
,指向的都是同一個i
,致使運行時輸出的是最後一輪的i
的值,也就是 10。
要是想獲得預期效果,能夠簡單的把var
換成let
。
let
實際上爲 JavaScript 新增了塊級做用域。
function f1() { let n = 5; if (true) { let n = 10; } console.log(n); // 5 }
上面的函數有兩個代碼塊,都聲明瞭變量n
,運行後輸出 5。這表示外層代碼塊不受內層代碼塊的影響。若是兩次都使用var
定義變量n
,最後輸出的值纔是 10。
塊級做用域的出現,實際上使得得到普遍應用的當即執行函數表達式(IIFE)再也不必要了。
// IIFE 寫法 (function () { var tmp = ...; ... }()); // 塊級做用域寫法 { let tmp = ...; ... }
若是不用let
,咱們可使用iife將setTimeout
包裹,從而達到預期效果。
for (var i = 0; i < 10; i++) { (i => setTimeout(() => { console.log(i); }, 1000))(i); }
for (var i = 0; i < 10; i++) { try { throw i; } catch (i) { setTimeout(() => { console.log(i); }, 1000); } }
What's the difference between resolve(thenable) and resolve('non-thenable-object')?