js經典面試題:setTimeout+for循環組合,使用閉包循環輸出1,2,3,4,5

這段時間重新溫習了一下js中的基礎知識:內存空間,執行上下文,變量對象,作用域,閉包,函數調用棧,隊列等等。當看到下面這道熟悉的經典for循環題目時,發現還有很多知識點理解的不到位,於是花時間利用以上知識,重新進行了對這道題目的深入剖析。

如下,利用閉包,讓循環輸出的結果爲1、2、3、4、5

for(var i=1; i<=5; i++){
	setTimeout(function timer(){
		console.log(i);
	},i*1000)
}

不賣關子,以上代碼執行後每隔1秒輸出一次6,共輸出5次。如下:
在這裏插入圖片描述
首先來看一下代碼的執行順序:
在這裏插入圖片描述
看代碼執行的順序就可以知道,console.log(i)並沒有在每次for循環的時候執行,而是在for循環的所有條件判斷執行完成之後纔開始執行,而這時候,i 的值已經變爲了6,而console.log(i)輸出的 i 就是for循環結束之後的 i 的值,所以會輸出5次6。i 定義的是一個全局變量,for循環每次獲取的 i 值都會覆蓋上次獲取的 i 的值。

那爲什麼setTimeout一定要等到for循環執行完成之後再執行呢?

這就與javascript中的事件循環機制有關了。

1、JavaScript代碼的執行過程中,除了依靠函數調用棧來搞定函數的執行順序外,還依靠任務隊列(task queue)來搞定另外一些代碼的執行。
2、一個線程中,事件循環是唯一的,但是任務隊列可以擁有多個。
3、setTimeout/setInterval等我們稱之爲任務源。而進入任務隊列的是他們指定的具體執行任務。

事件循環的順序,決定了JavaScript代碼的執行順序。它從script(整體代碼)開始第一次循環,之後全局上下文進入函數調用棧。直到調用棧清空(只剩全局),然後執行正在等待的任務隊列,執行完一個任務隊列後,如果還有任務隊列,那就執行該任務隊列,就這樣一直循環下去,直到所有代碼執行完畢。

上面代碼輸出結果的原因大致明白了,就是因爲for循環並沒有在每次循環的時候輸出 i 的值,而是在循環完成之後,i 已經變爲了6之後,才進行的輸出。

因爲事件循環機制的限制,以上代碼的執行順序是不可更改的,只要你使用了setTimeout,那就必須等到函數調用棧中可執行代碼執行完畢之後,再去執行任務隊列中的任務。既然代碼執行順序不可變,那可不可以把每次for循環時 i 的值保存起來,等到console.log(i)的時候再去調用保存好的 i 的值呢?這時候就需要使用閉包了。

修改後代碼:

for (var i = 1; i <= 5; i++) {
	(function (i) {
		setTimeout(function timer() {
			console.log(i);
		}, i * 1000)
	})(i)
}

這裏使用立即調用函數(IIFE)和匿名函數形成一個私有作用域(相當於閉包),私有作用域中的變量和全局作用域中的變量互不衝突,這種寫法也叫作"命名空間";這時每次for循環傳入的 i 的值都將作爲私有變量被保存在內存中,等待for循環執行完畢後,跟隨任務隊列輸出。

注:形成閉包之後,這裏的i其實是兩個不同的變量,for循環中的 i 爲全局變量,IIFE中的 i 爲私有變量。如果在全局狀態下console.log(i),最後會只會輸出一個6,如下代碼:

for (var i = 1; i <= 5; i++) {
	(function (i) {
		setTimeout(function timer() {
			console.log(i);
		}, i * 1000)
	})(i)
}
console.log(i);  //6 (最先輸出)

輸出結果:
在這裏插入圖片描述
閉包也稱爲函數嵌套函數,將修改的代碼寫入setTimeout中的方式更能直觀的體現閉包寫法:

for (var i = 1; i <= 5; i++) {
	setTimeout(function (i) {
		return function timer(){
			console.log(i);
		}
	})(i), i * 1000)
}

在ES6(ES2015)中,因爲新增了聲明變量的API,所以有更簡單的修改方式:將var修改爲let

for(let i=1; i<=5; i++){
	setTimeout(function timer(){
		console.log(i);
	}, i * 1000)
}

let聲明的變量的範圍會生成一個私有作用域,也叫作塊級作用域,該變量只會在當前作用域中生效,以 { } 爲標識,如下代碼:

{
	var a = 5;
	let b = 6;
	console.log(a); //5
	console.log(b); //6
}
console.log(a); //5
console.log(b); //b is not defined

參考資料:前端基礎進階

發佈了44 篇原創文章 · 獲贊 3 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章