一路上感謝各位讀者們的支持和回饋。
本 30 天系列文目前已經將篇幅重新整理、編纂成冊。
《JavaScript 概念三明治》在天瓏書局上架囉!
喜歡這個系列,想閱讀更詳細原理說明的讀者可以參考:
https://www.tenlong.com.tw/products/9789864347575
來講一下常用到的瀏覽器 API ,其實前面在講 Event Queue 的時候就已經提過 setTimeout 了,不過這邊就讓我們從更具實用性的層面來看這些方法。
this
in setTimeout / setInterval callback前面有提到 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 使用方式與 setTimeout 的語法相同,差在 setTimeout 只會執行一次,而 setInterval 則會根據開發者給的時間間隔,每隔一段時間執行一次。
setInterval(function(){console.log('da') },1000)
// print : da -> da -> da
由於 setTimeout
/ setInterval
函式本身會回傳一個計時器 id ,我們就可以把這個 id 記錄下來,當頁面要離開用不到的時候使用 clearTimeout
/ clearInterval
將他們清除:
let timerId = setInterval(function(){
console.log('do something')
},1000)
清除計時器在以前可能還不是會非常被注重的問題,但是像現在主流前端框架把渲染工作交給 JS ,如果使用虛擬路由來控制頁面切換的話,就算頁面切換了,JS 檔案也不會重新載入,主執行環境會一直存在,因此前面設定的計時器在不需要時如果沒有清除,就可能會造成頁面運算的負擔。
this
in setTimeout / setInterval callbacksetTimeout
/ 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
相等。
let boss = 'Yoda'
let user = {
name:'Luke',
introduce:function(){
setTimeout(()=>{
console.log('hey, ' + this.name)
},1000)
}
}
user.introduce() // print : hey,Luke
另外一個方法是,使用 Function.bind
,這個方法跟 call
或 apply
都可以指定函式執行環境內要綁定的 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
這段要講的大概是最經典的面試考題,只要講到跟 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
以參數形式傳入另外一個函式時,就會被函式執行環境保留而產生閉包。