---
# System prepended metadata

title: JavaScript - Scope 作用域 & Closure 閉包
tags: [Javascript, closure, scope]

---

# JavaScript - Scope 作用域 & Closure 閉包

## Scope 作用域

### 什麼是作用域

> 「作用域就是一個變數的生存範圍，一旦出了這個範圍，就無法存取到這個變數」。

當我們把變數 `a` **宣告在 function 中**，function 之外的地方都無法取用這個變數：
```javascript
// 把變數宣告在 function 中
function test1() {
    var a = "hello"
}
console.log(a) // Uncaught ReferenceError: a is not defined
```

但當我們把變數 `b` **宣告在全域**，function 裡面也可以取用外面的變數：
```javascript
// 把變數宣告在全域
var b = 123
function test2() {
  console.log(b) // 123
}
test2()
```


再看看這個例子：
```javascript
// 宣告函式
function outer() {
    var x = 5
    // 在函式裡面宣告另一個函式
    function inner() {
        var y = 4
        console.log(x+y) // 9
    }
    inner()
    console.log(y) // Uncaught ReferenceError: y is not defined
}
// 呼叫函式
outer()
```
- `y` 不能被 `outer` 取用：`y` 不在 `outer` function 的作用域中因此無法被 `outer` 取用
- 但 `x` 可以被 `inner` 取用：`inner` 在自己的作用域中找不到 `x` ，便開始向外尋找，最後在 `outer` 的作用域中取用 `x`

:::info
結論：
- 外面存取不到裡面的，但內層可以存取到外層的
- 倘若一直向外查找到全域都找不到變數，就會出現的 `ReferenceError: variable is not defined` 錯誤訊息
:::


### 靜態作用域 / 語法作用域

Javascript 的作用域是採用「靜態作用域（static scope）」，也稱作「語法作用域 (lexical scope)」（也有人翻成「詞法作用域」、「語彙作用域」）。「靜態作用域」是相對於「動態作用域(dynamic scope)」而來。

#### 靜態作用域
靜態作用域決定作用域的方式是根據**函式在哪裡被宣告**，與函式在哪裡被呼叫無關，查找變數的流程不會因為函式實際執行的位置不同而改變。

#### 動態作用域
動態作用域決定作用域的方式是以呼叫堆疊 (call stack) 為準，查找變數的流程是執行時才決定的。

來看看這個例子：
```javascript
var name = "Peter";  // 全域變數

function init() {
  var name = "Amy";  // 局部變數
    
  function displayName() {
    console.log(name); // "Peter" or "Amy"?
  }
    
  displayName();
}

init();
```
答案是 Amy。
1. `init()` 函式在自己的作用域建立了局部變數 `name` 以及 `displayName()` 函式
2. `displayName()` 函式的作用域中沒有局部變數 `name` ，因此開始向外層尋找，先找到了 `init` 作用域的 `name` 變數 --> 因此答案是 Amy

再看看另一個例子：

```javascript
var name = "Peter"; // 全域變數

function displayName() {
    console.log(name); // "Peter" or "Amy"?
}

function init() {
  var name = "Amy";  // 局部變數
  displayName();
}

init();
```
答案是 Peter。

- `displayName` 裡的 `name` 其實就是 global 的 `name`，跟 `init` 裡的 `name` 沒有關係。
- 這是因為 Javascript 是採用「靜態作用域」的關係， `displayName()` 是在全域中**被宣告**，即使是在 `init()` 中**被呼叫**，它的作用域仍與 `init()` 無關。（在某些採用「動態作用域」的程式語言中， `name` 確實有可能會是 `"Amy"`）。

:::info
這邊可以知道：
- 作用域跟這個 function 在哪裡被「呼叫」一點關係都沒有
- 靜態作用域是在 function 被「宣告」的時候就決定了，而不是 function 被「執行」的時候
:::
### 如何產生作用域
作用域有三種：
- 全域 Global Scope
- 函式作用域 Function Scope
- 塊級作用域 Block Scope (ES6)

### 全域 Global Scope
Javascript 執行的一開始就會創建一個全域環境，被定義在 function-scope 或是 block-scope 以外的變數就叫做全域變數 (global variable)。
- 不在函式或區塊內宣告的變數就是全域變數
- 可以在任何地方取用的變數
- **如果在定義變數時沒有加上宣告變數，即使在函式內也會成為全域變數**（應避免這種情形）
    ```javascript
    function hello() {
        name = 'Jack';
    }
    hello(); // 先執行 hello() 才宣告了 name 這個變數
    console.log(name); // Jack，即使變數是在函式中被定義，還是變成了全域變數
    ```

### 函式作用域 Function Scope
每建立一個函式就會創建一個新的函式作用域。
- 在函式中宣告的變數只能在函式（該作用域中）使用
- **不論是透過 `var`, `let`, `const` 宣告的變數，當他們在函式中宣告時都屬於函式作用域**

```javascript
function foo(){
    var num = 10; // function scope
}
console.log(num); // Uncaught ReferenceError: num is not defined
```

### 塊級作用域 Block Scope
在 ES6 中引入了 `let` 與 `const` 變數，這兩個變數的宣告方式提供了「區塊作用域」。
- 區塊作用域的範圍只存在於大括號 `{}`（例如 if 或 for）
- 區塊作用域與函式作用域相同，內部宣告的變數不能從外部取用
- **`var` 宣告的變數不會有區塊作用域，透過 `let` 與 `const` 宣告才能讓變數具有區塊作用域**

```javascript
// 在 {} 中用 let 或 const 宣告的變數具有區塊作用域
function foo1() {
    if (true) {
        const user = "花爸"; // block scope
    }
    console.log(user); // Uncaught ReferenceError: user is not defined
}
foo1();

// 在 {} 中用 var 宣告的變數不會有區塊作用域
function foo2() {
    if (true) {
        var user = "花媽"; // function scope
    }
    console.log(user); //花媽
}
foo2();
```
在上述的例子中，
- `foo1()` 使用了 `const` 宣告變數，所以 if 的 `{}` 內為區塊作用域，`const user = "花爸"` 只存在於該 `{}` 區塊中。既便是同函式中的 `console.log(user)` 也無法取用 if `{}` 內宣告的變數。
- `foo2()` 使用了 `var` 宣告變數，不會有區塊作用域，`var user = "花媽"` 存在於 `foo2` 函式中。因此同函式中的 `console.log(user)` 可以取用該變數。

### 作用域鏈 Scope Chain
Javascript 在使用一個變數的時候，會先在當層的作用域尋找該變數。若當前的作用域找不到該變數，會再往父層作用域尋找，就這樣一層一層往上找，一直到全局作用域如果還是沒找到就會報錯。這種由內而外搜尋的行為就是「作用域鏈」。

> 事實上，每個函式在執行時都會建立一個對應的作用域鏈。（延伸閱讀：[前端中階：JS令人搞不懂的地方-Closure(閉包)](https://hugh-program-learning-diary-js.medium.com/%E5%89%8D%E7%AB%AF%E4%B8%AD%E9%9A%8E-js%E4%BB%A4%E4%BA%BA%E6%90%9E%E4%B8%8D%E6%87%82%E7%9A%84%E5%9C%B0%E6%96%B9-closure-%E9%96%89%E5%8C%85-cbb9c6a4185c)）

```javascript
function outer() {
    var a = 10
    function inner() {
        console.log(a) // 10
    }
    inner()
}
outer()
```
上述例子中，`inner` 函式在自己的作用域中找不到 `a` 變數，就會往上一層的 `outer` 作用域找，如果還是找不到就會再往上一層找直到最上層的 `global` 作用域為止，如果最後還是找不到就會報錯。

查找方向：【 inner function scope 】 -> 【 outer function scope 】 -> 【 global scope 】，這個過程就構成了一個作用域鏈。

對 `inner` 來說，`a` 變數並不存在於它的作用域中，但它仍能存取這個變數，這樣的變數也叫做「**自由變數 (free variable)**」。


## Closure 閉包

### 什麼是 closure
根據 [MDN 的敘述](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Closures)：
> 閉包（Closure）是函式以及該函式被宣告時所在的作用域環境（lexical environment）的組合。

```javascript
const outer = () => {
    const greet = 'Hi'
    const inner = () => {
        console.log(greet) // 'Hi'
    }
    inner()
}

outer()
```
當我們呼叫 `outer` 時，就只是執行內部的一個 function `inner` 而已。

但如果我們不直接執行 `inner` ，而是直接把這個函式回傳：

```javascript
const outer = () => {
    const greet = 'Hi'
    const inner = () => {
        console.log(greet) // 'Hi'
    }
    return inner
}

const newFunc = outer() // 建立了 newFunc 實例(instance)
newFunc()
```

原本當函式執行完成以後，裡面宣告的變數也應該要被釋放，因此當 `outer()` 執行結束時，照理來說變數 `greet` 的記憶體空間會被釋放，但緊接著在呼叫 `newFunc` 時卻仍能存取到 `greet`。


換句話說只要 `newFunc` 還在， `greet` 就依然存在。這種**在 function 裡回傳了一個 function** 導致明明函式執行完畢卻還能存取到資源的現象就是「閉包」。


我們首先在 `outer` 函式中：
1. 宣告了一個變數 `greet`
2. 宣告一個 `inner` 函式，`inner` 函式會印出 `greet`
3. 回傳 `inner` 函式

接著把 `outer` 函式指派給 `newFunc` 變數，此時：
- `newFunc` 是 `inner` 在 `outer` 運行時所建立的實例 (instance)
- `newFunc` 存取了 `outer` 回傳的值（也就是 `inner` 函式）
- 由於 `inner` 的 instance 中保有了原本作用域的環境參照，而作用域裡含有 `greet` 變數，因此調用 `newFunc()` 時 `greet` 也能被取用


### Closure 的特性
- 在 JavaScript 中每當函式被建立時，一個閉包就會被產生
- 當一個 function 內 return 了另一個 function，通常就是有用到閉包的概念
- 閉包是資料結構的一種，當一個函式被宣告時，函式對其語法環境 (lexical environment) 的引用在一起的組合就是閉包（可以想像成函式記住了宣告時的作用域環境）
- 閉包就是可以回傳一個 function，並且使用父層 function 的變數
- 閉包可以讓函式內部的變數不受外部環境影響
- 閉包內部可以透過作用鏈獲取外部資料，內層作用域(child scope)永遠可以取得外層作用域(parent scope)的資料，反過來則不行


### Closure 的好處

#### 節省寫入記憶體的次數
可以把程式中需要重複執行的部分透過閉包封裝起來，進一步簡化程式，或是讓變數的值給保存下來。

1. 把想要回傳的東西包到 function 裡再回傳
2. 再把 function assign 給變數
3. 讓變數去存取 function 回傳的值

假設今天這個函式需要創造一個很大的 array，並且這個函式會被呼叫很多次：
```javascript
function heavyDuty(index) {
  const bigArray = new Array(7000).fill('something')
  console.log('created!')
  return bigArray[index]
}
heavyDuty(688) // created!
heavyDuty(688) // created!
heavyDuty(688) // created!
```

每執行一次函式都需要重複產生一個 7000 個 index 的 array，太浪費資源了，這時就很適合使用 closure：

```javascript
function heavyDutyClosure() {
  const bigArray = new Array(7000).fill(':)')
  console.log('created!')
  
  return function(index) { // 簡化寫法，直接 return function
    return bigArray[index]
  }
}

const getHeavyDuty = heavyDutyClosure()
getHeavyDuty(688) // created!
getHeavyDuty(688)
getHeavyDuty(688)
```

1. 把想要回傳的東西包到 function 裡再回傳
2. 把函式 `heavyDutyClosure` assign 給變數 `getHeavyDuty`，讓 `getHeavyDuty` 去存取 `heavyDutyClosure` 所回傳的值（也就是函式 `function(index)...`）
3. 執行 `getHeavyDuty` 時，就是在呼叫函式 `function(index)...`

值得注意的是，雖然呼叫了三次 `getHeavyDuty` 但用 closure 寫法的版本只會在第一次印出 `created!` 

執行第一次 `getHeavyDuty` 時的運作流程：
```
呼叫 getHeavyDuty
-> 呼叫 heavyDutyClosure
-> 創造 bigArray 並印出 created!
-> 回傳函數
-> 執行回傳的函數
-> 回傳 bigArray[688]
```

執行第一次後 `function(index)...` 就已經被存取 `getHeavyDuty` 這個變數了。後續在執行 `getHeavyDuty` 就是在執行 `function(index)...` 了。而 `heavyDutyClosure` 中沒有被 return 的部分（產生 7000 個 array、印出 created!）就只會執行一次。

接下來幾次執行 `getHeavyDuty` 的運作流程其實只剩：
```
呼叫 getHeavyDuty
-> 呼叫函數 function(index)… 
-> 回傳 bigArray[688]
```

#### 封裝(Encapsulation) 變數 (private variable)

封裝是物件導向程式設計(Object Oreinted Programming，簡稱 OOP) 一個非常重要的概念。

有些資料如果不想讓外部函式/方法改動它，就可以使用閉包的方式來確保只有內部函式可以改動內部資料。

```javascript
let count = 0
function increment(num) {
  return count += num
}
function decrement(num) {
  return count -= num
}
function getCount() {
  return count
}
increment(3)
increment(3)
increment(1)
getCount() // 7
```
這裡直接在全域宣告 `count` 變數是錯誤的，可能會導致程式碼出現不可預期的錯誤。比如要是有人在其他地方寫了 `count = 5` 資料就亂掉了。

這種情況就可以使用閉包，寫一個 function 把 `count` 這個變數和能夠改變這個變數的 function 封裝在一起：

```javascript
function createCounter (initCount) {
  let count = initCount
  return {
    increment: (num) => count += num,
    decrement: (num) => count -= num,
    getCount: () => count
  }
}

const counter = createCounter(0)

counter.increment(3) // 3
counter.increment(3) // 6
counter.increment(1) // 7
counter.getCount() // 7
count = 5 // ReferenceError: count is not defined
```
這樣一來除了 `.increment()` 、`.decrement()` 、`.getCount()` 之外的方法都無法改變 `count` 這個變數。

- `count` 是一個 **private variable**
- `count` 只存在於 `createCounter` 作用域中
- `count` 只能被內部的函式改動資料，不會被外部環境所影響
- 能夠存取 private variable 的方法被稱作 **privileged method**

#### 記憶體回收 (Garbage Collection)


## 作用域陷阱＆閉包應用

```javascript
var btns = document.querySelectorAll('button')
for(var i=0; i<btns.length; i++){
    btns[i].addEventListener('click', function(){
        alert(i+1)
    })
}
```
假設頁面上有 3 個按鈕，預期的行為是點擊第一個按鈕跳出 1 、點擊第二個按鈕跳出 2 ⋯以此類推。但實際操作後會發現，無論點擊哪一個按鈕都會印出 3。

原本想像中的迴圈應該是要這樣跑的：
```javascript
btn[0].addEventListener('click', function(){
    alert(1)
})

btn[1].addEventListener('click', function(){
    alert(2)
})

btn[2].addEventListener('click', function(){
    alert(3)
})
```
但實際上 JS 引擎在運作時是這樣跑的：
```javascript
btn[0].addEventListener('click', function(){
    alert(i+1)
})

btn[1].addEventListener('click', function(){
    alert(i+1)
})

btn[2].addEventListener('click', function(){
    alert(i+1)
})
```

在跑迴圈的時候只是加上它的 callback function 而已還沒有執行，是要等到使用者按按鈕的時候才會去尋找 `i` 變數。

也就是說，事件發生時（使用者點擊按鈕）所引發的函式 (callback function) 是在迴圈跑完之後才執行。


加上這幾個 callback function 本身並沒有 `i` 這個變數，因此會向作用域的外層開始尋找，一直找到在迴圈中定義的 `i` （定義在全域中）作為其值。

而此時的 `i` 早就已經變成了 2 了，因此無論點擊哪一個按鈕，取用的都是同一個全域變數中的 `i`，所以都只會輸出 3。

這個問題可以透過閉包來解決：

**建立一個新的函式並傳入參數：**
```javascript
function getAlert(num) {
  return function() {
    alert(num)
  }
}
for(var i=0; i<btn.length ; i++) {
  btn[i].addEventListener('click', getAlert(i))
}
```

透過高階函式 (Higher Order Function) 產生三個新的 function，並且因為傳入了一個參數 `i`，利用閉包的特性將 `i` 個別鎖進 `getAlert` 中。

**或者也可以利用 IIFE(Immediately Invoked Function Expression)：**
```javascript
for(var i=0; i<btn.length ; i++) {
    (function(num) {
        btn[i].addEventListener('click', function(){
            alert(num)
        })
    })(i)
}
```

透過 IIFE 把 function 包起來並傳入參數 `i` 立即執行。

迴圈每跑一次就會產生一個新的 function 並且立刻呼叫它，因此會就地產生新的作用域，並且利用了閉包的特性將參數 `i` 鎖住。

**也可以直接把 `var` 改成 `let` (ES6)**：
```javascript
for(let i=0; i<btn.length ; i++){
    btn[i].addEventListener('click', function() {
    alert(i)
  })
}
```
ES6 裡有了塊級作用域 (block scope) 之後就可以直接利用 `let` 的特性，等於每跑一次迴圈都會產生一個新的作用域，因此 `i` 就會存在在該作用域裡。可以看成這樣：

```javascript
{ // 塊級作用域
  let i=0
  btn[i].addEventListener('click', function() {
    alert(i)
  })
}
{ // 塊級作用域
  let i=1
  btn[i].addEventListener('click', function() {
    alert(i)
  })
}
...
```


## Ref
- [Javascript 的作用域 (Scope) 與作用域鏈 (Scope Chain) 是什麼?](https://www.explainthis.io/zh-hant/interview-guides/javascript/what-is-scope-and-scope-chain)
- [[筆記]-JavaScript 作用域與作用域鏈是什麼?關於作用域的4個觀念](https://jianline.com/javascript-scope-and-scope-chain/#%E5%85%A8%E5%9F%9F%E4%BD%9C%E7%94%A8%E5%9F%9FGlobal_Scope)
- [[JS基礎] 什麼是閉包(Closure)？](https://johnnychang25678.medium.com/js%E5%9F%BA%E7%A4%8E-%E4%BB%80%E9%BA%BC%E6%98%AF%E9%96%89%E5%8C%85-closure-61bcc1eb02ca)
- [[JS] 深入淺出 JavaScript 閉包（closure）](https://pjchender.dev/javascript/js-closure/)
- [所有的函式都是閉包：談 JS 中的作用域與 Closure](https://github.com/aszx87410/blog/issues/35)
- [前端中階：JS令人搞不懂的地方-Closure(閉包)](https://hugh-program-learning-diary-js.medium.com/%E5%89%8D%E7%AB%AF%E4%B8%AD%E9%9A%8E-js%E4%BB%A4%E4%BA%BA%E6%90%9E%E4%B8%8D%E6%87%82%E7%9A%84%E5%9C%B0%E6%96%B9-closure-%E9%96%89%E5%8C%85-cbb9c6a4185c)
- [閉包包什麼？探索JS中的作用域與Closure | Javascript鍛鍊日記](https://medium.com/%E7%8B%97%E5%A5%B4%E5%B7%A5%E7%A8%8B%E5%B8%AB/%E9%96%89%E5%8C%85%E5%8C%85%E4%BB%80%E9%BA%BC-%E6%8E%A2%E7%B4%A2js%E4%B8%AD%E7%9A%84%E4%BD%9C%E7%94%A8%E5%9F%9F%E8%88%87closure-javascript%E9%8D%9B%E9%8D%8A%E6%97%A5%E8%A8%98-f7b1a2ac1e2a)