在這之前先要了解一下
for迴圈中let 和var的區別
var 是函式級作用域或者全域性作用域,let是塊級作用域 看一個例子
function foo() {
for (var index = 0; index < array.length; index++) {
//..迴圈中的邏輯程式碼
}
console.log(index);//=>5
}
foo()
console.log(index)//Uncaught ReferenceError: index is not defined
複製程式碼
foo函式下的index輸出5,全域性下的index不存在 現在我們把var 換為let
function foo() {
for (let index = 0; index < array.length; index++) {
//..迴圈中的邏輯程式碼
}
console.log(index)//Uncaught ReferenceError: index is not defined
}
foo()
複製程式碼
報錯了,index不在foo函式作用域下,當然肯定也不會再全域性下 因為var和let的這個區別(當然var和let的區別不止於此)所以導致了下面的這個問題 關於var的
const array = [1, 2, 3, 4, 5]
function foo() {
for (var index = 0; index < array.length; index++) {
setTimeout(() => {
console.log(index);
}, 1000);
}
}
foo()
複製程式碼
關於let的
const array = [1, 2, 3, 4, 5]
function foo() {
for (let index = 0; index < array.length; index++) {
setTimeout(() => {
console.log(index);
}, 1000);
}
}
foo()
複製程式碼
因為var和let 在作用域上的差別,所以到這了上面的問題
使用var 定義變數的時候,作用域是在foo函式下,在for迴圈外部,在整個迴圈中是全域性的,每一次的迴圈實際上是為index賦值,迴圈一次賦值一次,5次迴圈完成,index最後的結果賦值就為5;就是被最終賦值的index,就是5;
let的作用局的塊級作用局,index的作用域在for迴圈內部,即每次迴圈的index的作用域就是本次迴圈,下一次迴圈重新定義變數index;所以index每次迴圈的輸出都不同
這裡還有另外一個問題,setTimeout,這是一個非同步,這就是我們今天要討論的
迴圈中的非同步
setTimeout(func,time)函式執行機制
setTimeout(func,time)是在time(毫秒單位)時間後執行func函式。瀏覽器引擎按順序執行程式,遇到setTimeout會將func函式放到執行佇列中,等到主程式執行完畢之後,才開始從執行佇列(佇列中可能有多個待執行的func函式)中按照time延時時間的先後順序取出來func並執行。即使time=0,也會等主程式執行完之後,才會執行。
一個需求,一個陣列array[1,2,3,4,5],迴圈列印,間隔1秒
上面的let是迴圈列印了12345,但是不是間隔1s列印的,是在foo函式執行1s後,同時列印的
方式一 放棄for迴圈,使用setInterval
function foo(){
let index = 0;
const array = [1, 2, 3, 4, 5]
const t = setInterval(()=>{
if (index < array.length) {
console.log(array[index]);
}
index++;
}, 1000);
if (index >= array.length) {
clearInterval(t);
}
}
foo()
複製程式碼
我們上面說到,當for迴圈遇到了var,變數index的作用域在foo函式下,迴圈一次賦值一次,5次迴圈完成,index最後的結果賦值就為5;就是被最終賦值的index,就是5;
方式二,引入全域性變數
程式碼執行順序是,先同步執行for迴圈,再執行非同步佇列,在for迴圈執行完畢後,非同步佇列開始執行之前,index經過for迴圈的處理,變成了5。 所以我們引入一個全域性變數j,使j在for迴圈執行完畢後,非同步佇列開始執行之前,依然是0,在非同步執行時進行累加
var j = 0;
for (var index = 0; index < array.length; index++) {
setTimeout(() => {
console.log(j);
j++;
}, 1000 * index)
}
複製程式碼
方式三 for迴圈配合setTimeout(常規思路,不贅述,面試必備技能)
const array = [1, 2, 3, 4, 5]
function foo() {
for (let index = 0; index < array.length; index++) {
setTimeout(() => {
console.log(index);
}, 1000*index);
}
}
foo()
複製程式碼
方式四,通過閉包實現
開始討論方式四之前我推薦先閱讀一遍我之前寫過一篇文章 談一談javascript作用域 我們對上面的問題再次分析,for迴圈同步執行,在for迴圈內部遇到了setTimeout,setTimeout是非同步執行的,所以加入了非同步佇列,當同步的for迴圈執行完畢後,再去執行非同步佇列,setTimeout中有唯一的一個引數數index 方式三可行,是因為let是塊級作用域,每次for執行都會建立新的變數index,for迴圈執行完畢後,非同步執行之前,建立了5個獨立的作用域,5個index變數,分別是0,1,2,3,4,相互獨立,互不影響,輸出了預期的結果 如果說每次迴圈都會生成一個獨立的作用域用來儲存index,問題就會得到解決,所以,我們通過閉包來實現
const array = [1, 2, 3, 4, 5]
function foo() {
for (var index = 0; index < array.length; index++) {
function fun(j) {
setTimeout(function () {
console.log(j);
}, 1000 * j);
}
fun(index)
}
}
foo()
複製程式碼
setTimeout中的匿名回撥函式中引用了函式fun中的區域性變數j,所以當fun執行完畢後,變數j不會被釋放,這就形成了閉包 當然我們可以對此進行一下優化
const array = [1, 2, 3, 4, 5]
function foo() {
for (var index = 0; index < array.length; index++) {
(function(j) {
setTimeout(function () {
console.log(j);
}, 1000 * j);
})(index)
}
}
foo()
複製程式碼
將foo函式改為匿名的立即執行函式,結果是相同的
總結
for迴圈本身是同步執行的,當在for迴圈中遇到了非同步邏輯,非同步就會進入非同步佇列,當for迴圈執行結束後,才會執行非同步佇列 當非同步函式依賴於for迴圈中的索引時(一定是存在依賴關係的,不然不會再迴圈中調動非同步函式)要考慮作用域的問題, 在ES6中使用let是最佳的選擇, 當使用var時,可以考慮再引入一個索引來替代for迴圈中的索引,新的索引邏輯要在非同步中處理 也可以使用閉包,模擬實現let 在實際開發過程中,迴圈呼叫非同步函式,比demo要複雜,可能還會出現if和else判斷等邏輯,具體的我們下次再續
參考
《你不知道的JavaScript》上卷