很多技術筆記說明 JS變數的作用域時,都會使用迴圈與一個 JS內建函式來說明作用域的不同之處,但為什麼使用了 setTimeout() 這個語法,可以讓結果不同?
主要是因為以下的運作方式與觀念,會讓結果不同 : 
- 變數宣告影響作用域
 
- 執行緒
 
- Callback Function 
 
迴圈與變數作用域
在以下的例子中,是最常見的 for迴圈,使用不同的變數宣告方式並印出結果,而迴圈宣告的變數會建立在不同地方,假設宣告變數在在全域環境中,那麼
- 使用 var宣告的變數,被建立在全域環境底下。
 
- 使用 let宣告的變數,被建立在獨立的區塊中,也就是該函式的執行環境。
 
直接印出迴圈結果
1 2 3 4 5 6 7 8 9 10 11 12 13
   | for (var i = 0 ; i < 5 ; i++) {   console.log(i) }
  for (let y = 0 ; y < 5 ; y++) {    console.log(y) }
 
 
 
 
 
 
  | 
 
一秒後印出所有結果
- 使用 JS內建函式 
setTimeout(),卻讓執行結果改變。 
- 使用 var宣告變數
- var的最小作用域為 function
 
- 在全域環境下,for迴圈每跑一次,事件佇列就會多一個 
setTimeout(),當 for迴圈跑完換事件佇列內的 setTimeout() 執行時,i 已經改變成 5 了,所以會有五個 5。 
- 但是,為什麼結果是 5 不是 4 ?
 
- i 已經在全域環境下建立了,也因此執行第一次迴圈時,i++的關係,i 就變成了 1,總共要執行五次,所以 i 為 5。
 
 
1 2 3 4 5 6 7 8 9 10
   | for (var i = 0 ; i<5 ; i++) {   setTimeout(()=>{     console.log(i)   }, 1000) }
 
 
 
 
 
  | 
 
- 使用 let宣告變數
- let的最小作用域為 
{ } ,例如 for(){}、if(){} 
- 每當進入事件佇列時,當下的 y 值會跟著進去
 
 
1 2 3 4 5 6 7 8 9 10
   | for (let y = 0 ; y<5 ; y++) {   setTimeout(()=>{     console.log(y)   }, 1000) }
 
 
 
 
 
  | 
 
上述的例子,有說到 setTimeout() 會進入事件佇列,接著因變數宣告方式不同會改變結果,這跟接下來的說明有關。
Callback Function 與執行緒
函式的參數不僅可以放單一值、陣列以及物件,也可以放函式,而 JS的執行方式會影響函式的運作時機。
Callback Function
- 某個函式做為另一個函式的參數值。
 
- 例如 
setTimeout() 
setTimeout( 某函式, 等待多久後印出的時間 ) 
setTimeout( function(), 1000 ) 
- 若依賴其他函式過深,也就是重複這種型態,就會變成所謂的波動拳。
 
- 衍伸出 Promise 的概念。
 
 
1 2 3 4 5
   |  setTimeout(()=>{   console.log('Callback Function') }, 1000)
 
 
  | 
 
執行緒
- JS執行方式為單線程
 
- 執行過程
- 由上到下執行。
 
- 執行函式時,會先放進執行堆疊,直到 return結果後離開(以下範例用 
console.log() 代替)。 
- 執行事件時,會等待被觸發,例如點擊,計時器之類的事件,它們會暫時在瀏覽器中等待。
 
- 當事件被觸發時, JS會將它要執行的函式放進事件佇列,等執行堆疊的函式處理完畢後,再處理佇列內的函式。
 
- 為什麼會有事件?
setTimeout(),可以寫成 window.setTimeout(),也就是說,事件是瀏覽器的 API給 JS使用的。 
 
- 程式碼的事件 > 等待觸發 > 觸發 > 事件佇列
 
 
測試範例
1 2 3 4 5 6 7 8 9 10 11
   | 
  const fnA = () => {   console.log('我是函式 A') } const fnB = () => {   console.log('我是函式 B') } fnA() fnB()
 
 
  | 
 
1 2 3 4 5 6 7 8 9 10 11
   | 
  const fnA = () => {   console.log('我是函式 A')   fnB() } const fnB = () => {   console.log('我是函式 B') } fnA()
 
 
  | 
 
1 2 3 4 5 6 7 8 9 10 11
   | 
  const fnA = () => {   fnB()   console.log('我是函式 A') } const fnB = () => {   console.log('我是函式 B') } fnA()
 
 
  | 
 
1 2 3 4 5 6 7 8 9 10 11
   | 
  const fnA = () => {   console.log('我是函式 A')   setTimeout(fnB, 3000) } const fnB = () => {   console.log('我是函式 B') } fnA()
 
 
  | 
 
- 測試五
- JS的執行方式若遇到事件時,會等待事件觸發。
 
- 直到執行堆疊處理完畢後,看事件是否觸發,若觸發則放入事件佇列中並執行事件內的函式。
 
- 也就是說,無論 
setTimeout() 秒數設為 0秒還是3秒,都會先等待觸發並往下處理執行堆疊內的函式。 
 
1 2 3 4 5 6 7 8 9 10 11 12
   |  const fnA = () => {   setTimeout(fnB, 3000)   console.log('我是函式 A') } const fnB = () => {   console.log('我是函式 B') } fnA()
 
 
 
 
  | 
 
回想
以下面兩個例子來說,它包含了 : 
- var的作用域
- 用 function區隔會有不同結果,可以做到與 let相同的事。
 
 
- 執行緒
 
- Callback Function
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
   | 
 
 
  const fnA = (y) => {   for (var z = 0 ; z < y ; z++) {   setTimeout(()=>{      console.log(z)              }, 1000*z)   }   console.log('我是函式 A,接著執行函式 B') } fnA(5)
 
 
 
 
 
 
  | 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
   | 
 
 
  const fnA = (y) => {   for (var z = 0 ; z < y ; z++) {     fnB(z)   }   console.log('我是函式 A,接著執行函式 B') } const fnB = (x) => {   setTimeout(()=>{      console.log(x)             }, 1000*x) }  fnA(5)
 
 
 
 
 
 
  | 
 
延伸概念
參考來源
- Kuro Hsu - 重新認識 JavaScript: Day 18 Callback Function 與 IIFE
 
- Kuro Hsu - 重新認識 JavaScript: Day 19 閉包 Closure
 
- PJCHENder那些沒告訴你的小細節 - [筆記] 理解 JavaScript 中的事件循環、堆疊、佇列和併發模式(Learn event loop, stack, queue, and concurrency mode of JavaScript in depth)