---
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核心篇]