---
# System prepended metadata

title: 不同的變數宣告有不同的結果 - 迴圈變數與作用域
tags: [JavaScript]

---

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