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