JS 原力覺醒 Day26 - 常用 API: setTimeout / setTimeInterval

來講一下常用到的瀏覽器 API ,其實前面在講 Event Queue 的時候就已經提過 setTimeout 了,不過這邊就讓我們從更具實用性的層面來看這些方法。
https://ithelp.ithome.com.tw/upload/images/20190916/20106580lJIWdcHc2t.png

Outline

  • setTimeout / setInterval 使用
  • setTimeout / setInterval 清除
  • this in setTimeout / setInterval callback
  • 解決回呼函式內 this 的問題
  • setTimeout / setInterval 、迴圈與 Closure

setTimeout / setInterval 使用

setTimeout

前面有提到 setTimeout 的基本使用方式,而第一個參數傳入的 callback 會被推送到 Event Queue ,待主執行環境堆疊清空以後,才會被執行 ,所以就算第二個參數設定的時間是 0 秒,也不會立刻執行。

function step(stepNum){
	console.log(`step${stepNum}`)
}

step('1') 
setTimeout(function(){step('2')},0)
step('3')

// will print: step1 --> step3 --> step2

setInterval

setInterval 使用方式與 setTimeout 的語法相同,差在 setTimeout 只會執行一次,而 setInterval 則會根據開發者給的時間間隔,每隔一段時間執行一次。

 setInterval(function(){console.log('da')  },1000)
// print : da -> da -> da

setTimeout / setInterval 清除

由於 setTimeout / setInterval 函式本身會回傳一個計時器 id ,我們就可以把這個 id 記錄下來,當頁面要離開用不到的時候使用 clearTimeout / clearInterval 將他們清除:

let timerId = setInterval(function(){
	console.log('do something') 
},1000) 

清除計時器在以前可能還不是會非常被注重的問題,但是像現在主流前端框架把渲染工作交給 JS ,如果使用虛擬路由來控制頁面切換的話,就算頁面切換了,JS 檔案也不會重新載入,主執行環境會一直存在,因此前面設定的計時器在不需要時如果沒有清除,就可能會造成頁面運算的負擔。

this in setTimeout / setInterval callback

setTimeout / setInterval 裡第一個回呼函式內的 this 如果沒有經過處理的話,預設都是指向全域環境 window ,因為這兩個都是屬於 window 物件底下的函式,我們可以推斷我們傳進去的回呼函式是在裡面被執行。雖然沒辦法直接看到 setTimeout 裡面的原始碼,不過可以推斷內容大概是像這樣 ,下面以 Pseudo code 示意:

 window  = {
		... 
		setTimeout:function(timerFunc,time){
			//several minutes later...
			timerFunc() 
		} 
}

之前提過在思考 this 的連結的時候,有提到,「如何呼叫」函式將會影響 this 的指向,想一想「隱含」的繫結, 再對比上面的 setTimeout 內容,可以看出我們傳入的回呼函式在 setTimeout 被呼叫,但因為是直接呼叫,沒有隱含繫結,因此在內的 this 會指向全域。

解決回呼函式內 this 的指向問題

承上一段,那要怎麼樣才能讓 this 指向目前所屬的執行環境,讓開發者在撰寫程式碼的時候更不容易誤解?

方法一

有一個方法是:使用箭頭函式,因為箭頭函式內沒有 this ,更準確來說, 箭頭函式內的 this 與他外部語彙範疇的 this 相等。

let boss = 'Yoda'
let user = {
	name:'Luke',
	introduce:function(){
		setTimeout(()=>{
			console.log('hey, ' + this.name) 
		},1000) 
	} 
} 
user.introduce() // print : hey,Luke

方法二

另外一個方法是,使用 Function.bind,這個方法跟 callapply 都可以指定函式執行環境內要綁定的 this ,差別在呼叫 bind 後會回傳一個全新、綁定過 this 的函式。

let user = {
	name:'Luke',
	introduce:function(){
		setTimeout(getName.bind(this),1000) 
	} 
} 

function getName(){
	console.log('hey, '+ this.name)	
} 

user.introduce() // print : hey,Luke

setTimeout / setInterval 、迴圈與 Closure

這段要講的大概是最經典的面試考題,只要講到跟 Closure 有關的問題,通常一定會提到迴圈。
先來看這段例子:

for(var i =0;i<10;i++){
	setTimeout(
		function (){
			console.log(i)
	},1000) 
}

在一秒過後我們就很驚訝的會發現, JS 吐出了 10 個 10 給我們,這是因為 var 宣告是屬於 function scope 但是 for 迴圈並不是 function ,所以在之內宣告的變數 i 就等於是全域變數。也因此無法透過 fucntion 產生函式執行堆疊或閉包,於是這個回呼函式會被推到 Event Queue,待時間到要執行,去獲取i 的時候,全域的 i 早就已經被 for 迴圈修改而成為10了 ,所以才會有這樣子的結果

解方:

要解決這個問題我們只要想辦法讓維持 setTimeout 回呼函式與每個 i 的聯繫即可,還記得 let 屬於 block scope ?所以用 let 產生的變數是綁在會在不同的 block 上 ,對 for 回圈來說,每次 i+1 的迴圈迭代之後的,都是一個新的 block,再搭配 block scope 的特性,就可以在每個 block 留下與每個 i 的連結:

for(let i =0;i<10;i++){
	setTimeout(
		function (){
			console.log(i)
	},1000) 
}

// print : 1,2....10

或是ㄧ樣利用 function scope 的特性:

for(var i=0;i < 10; i++){
	getValueOf_i(i)
}

function getValueOf_i(i){
	setTimeout(function(){
		console.log(i)
	},1000) 
} 

這樣一來當 i 以參數形式傳入另外一個函式時,就會被函式執行環境保留而產生閉包。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×