JS中的for循環——你可能不知道的點。
- 2019 年 10 月 28 日
- 筆記
提出問題
問題1:
看一段for循環的程式碼,大家先想一下執行結果是什麼?
var arr = [2,4,6,8,10]; var arrLength = arr.length; for (var i = 0; i < arrLength; i++) { setTimeout(function() { console.log(i); console.log(arr[i]); }, 2000); }
問題2:
for循環中出現多個非同步函數(比如ajax請求,或者node後端執行一些資料庫操作或文件操作),如果想要這些非同步串列變為同步應該怎麼做?
問題1解決與相關講解
結果
預期結果
0 2 1 4 2 6 3 8 4 10
運行後的結果
5 undefined 5 undefined 5 undefined 5 undefined 5 undefined
產生結果的原因
setTimeout()函數回調屬於非同步任務,會出現在宏任務隊列中,被壓到了任務隊列的最後,在這段程式碼應該是for循環這個同步任務執行完成後才會輪到它,所以for循環在遍歷過程中i不斷加1,直到i判斷失敗一次才停止,這時候i為5,也就是說空跑了5次循環。等到了setTimeOut預定的時間後就會執行在for遍歷過程中聲明的5個setTimeout。所以最終運行後會出現上面的結果,與預期結果不符。
註:關於宏任務隊列,同步任務等相關的問題,如果有問題,可以查看我的另一篇文章一道面試題引發的事件循環深入思考詳細了解。
正確執行的解決方案
1. 閉包,立即執行函數
想要得到預期的結果,第一種辦法是使用閉包,在閉包函數內部形成了局部作用域,每循環一次,形成一個自己的局部作用域,不受外部變數變化的影響。程式碼如下:
var arr = [2,4,6,8,10]; var arrLength = arr.length; for (var i = 0; i < arrLength; i++) { (function(i) { setTimeout(function() { console.log('i是' + i); console.log('value是' + arr[i]); }, 2000); })(i); }
2. let
將程式碼中的var改成let,let非常適合用於 for循環內部的塊級作用域。JS中的for循環體比較特殊,每次執行都是一個全新的獨立的塊作用域,用let聲明的變數傳入到 for循環體的作用域後,不會發生改變,不受外界的影響。
程式碼如下:
var arr = [2,4,6,8,10]; var arrLength = arr.length; // i雖然在全局作用域聲明,但是在for循環體局部作用域中使用的時候,變數會被固定,不受外界干擾。 for (let i = 0; i < arrLength; i++) { (function(i) { setTimeout(function() { console.log('i是' + i); console.log('value是' + arr[i]); }, 2000); })(i); }
問題2解決與相關講解
for循環中使用非同步,在node.js後端開發或者前端ajax請求的時候還是比較常見的。有多種解決方案
- 回調 callback 嵌套非同步操作、再回調的方式
- Promise + then() 層層嵌套
- async和await
選擇我個人認為最優秀的解決方式3async和await進行講解。
async + await 「外異內同」
例子:
如果要去將一批數據發送到伺服器,只有前一批發送成功(即伺服器返回成功的響應),才開始下一批數據的發送,否則終止發送。這就是一個典型的 「for 循環中存在相互依賴的非同步操作」 的例子
例子對應偽程式碼:
async function task () { for (let val of [1, 2, 3, 4]) { // await 是要等待響應的 let result = await send(val); if (!result) { break; } } } task();
偽程式碼中使用await之後,實現了非同步變成同步的轉化,只有for循環中當次對應的發送請求完成且獲取結果,才會繼續往下執行。
await幾點說明:
- await執行的那一行語句是同步的。
- async函數執行後,總是返回一個promise對象,可以理解為這個函數是一個非同步函數(外異)但是———————-引用阮一峰老師書中一句話:
當函數執行的時候,一旦遇到 await 就會先返回,等到觸發的非同步操作完成,再接著執行函數體內後面的語句。
我對阮一峰老師的話再具體說明一下,可能有些同學還不是特別理解。實際上我們調用了await,這時候await這條語句下面的語句已經不會執行了(內同),而是先給外層async函數返回了一個promise對象,await後面對應的應該也是一個promise對象只有該對象 resolve 掉,產生結果,await 那一行程式碼才算真正執行完,才繼續往下走。(注意:await執行之後應該是一個resolve的結果而不是promise對象了)。
node.js後端開發-await在for循環中的應用
看一段後端項目中應用await的程式碼:
//dayResult是一個查詢到的數組 for (const item of dayResult) { //TODO 查詢用戶vip表 用戶體驗vip距離到期的用戶列表 let userIds=await db.vip.findAll({ where:{ experience_time:{ '$lte':moment().subtract(15-item.day,'day').endOf('day') ,//獲取四天前都0時0分秒 '$gte':moment().subtract(15-item.day,'day').startOf('day') ,//獲取四天前都0時0分秒 }, vip_type:0 }, attributes:['user_id',Sequelize.literal(`'${item.id}' as notice_id`)], raw:true }); userNoticeRecord=userNoticeRecord.concat(userIds) }