---
# System prepended metadata

title: 閉包
tags: [JavaScript]

---

---
tags:  JavaScript
title: 閉包
---

# 閉包
閉包為範圍鏈、記憶體空間、表達式以及函式等觀念總合的概念，重複執行函式時，其變數值就會累加上去。要利用閉包的概念前，先來複習一下函式還有怎樣的特性。
- 函式與範圍鏈
- 函式與提升
- 函式與記憶體空間

## 函式與範圍鏈
在函式中的變數，若沒有宣告，則往外層尋找直到全域，若全域也沒有，則報錯。

### 靜態作用域
跟在哪裡被呼叫無關，跟位置有關。
來看以下範例，函式的位置會影響最後的結果。
```javascript 
var a = 1
function fnA(){
  console.log(a)
}
function fnB(){
  var a = 2
  fnA()
}
fnB() // 1
// fnA()內本身沒有 a變數，則向外層找
// 它也不在 fnB()內，於是找到了全域物件中的變數 a

function fnC(){
  var a = 3
  function fnD(){
    console.log(a)
  }
  fnD()
}
fnC() // 3
// fnD()本身沒有 a變數，則向外層找
// 在 fnC()找到已宣告的變數 a
```

### 動態作用域
跟在哪裡使用無關，跟在哪裡被呼叫有關，最常見的就是 `this` 的運用。

## 函式與提升(Hoisting)
在函式中 :
- 將自訂參數重新宣告不會有任何作用
- 函式中的函式，不會提升至外層
- 若在函式中不將變數宣告或者非自訂參數，則可能依據呼叫環境在全域物件中建立變數
```javascript
function fn(a){
  console.log(a)
  
  // 將自訂參數重新宣告不會有任何作用
  var a
  console.log(a)
  
  // 賦予新值
  a = 1
  console.log(a)
  
  // 若在裏頭插入函式，它不會提升到外層
  function fnB(){
    console.log(a)
  }
  fnB()
  b = 2
}
fn('我是函式')
console.log(a)
console.log(b)
// '我是函式'
// '我是函式'
// 1
// 1
// a is not defined 
// 2，b未正式宣告且因為範圍鏈(向外層尋找)的關係，在全域物件中建立了變數
```


## 函式與記憶體空間
先看下面的例子，函式每次執行時，都會加 1。
```javascript 
var a = 0
function fn(){
  a += 1
  console.log(a)
}
fn() // 1
fn() // 2
fn() // 3
fn() // 4
fn() // 5
```

如果將變數 a，移動到函式內，重複執行函式時，就會因為記憶體空間被釋放掉，又重新宣告而無法累計。
```javascript 
function fn(){
  var a = 0
  a += 1
  console.log(a)
}
fn() // 1
fn() // 1
fn() // 1
fn() // 1
fn() // 1
```

接下來，改寫上述範例，假設今天去夜市套圈圈，老闆的每個籃子裡總共有五個圈圈，規則是每次只能丟一個圈圈。今天有兩個玩家，都另開新局，他們都能有五個圈圈可投。
- return 一個函式，此函式包含著變數，該變數會視為還須利用而不會被上層的函式釋放掉
- 分別宣告變數並賦予函式，函式內部所宣告的變數不會互相影響
```javascript 
function game(){
  var times = 5
  return function(){
    times -= 1
    return `圈圈剩餘 ${times} 個`
  }
}
var personA = game()
console.log(personA()) // 圈圈剩餘 4個
console.log(personA()) // 圈圈剩餘 3個
console.log(personA()) // 圈圈剩餘 2個
console.log(personA()) // 圈圈剩餘 1個
console.log(personA()) // 圈圈剩餘 0個

var personB = game()
console.log(personB()) // 圈圈剩餘 4個
console.log(personB()) // 圈圈剩餘 3個
console.log(personB()) // 圈圈剩餘 2個
console.log(personB()) // 圈圈剩餘 1個
console.log(personB()) // 圈圈剩餘 0個
```

### 閉包與函式工廠
這次老闆使用機器人自動丟五個圈圈出去，每次丟一個就紀錄一次剩餘多少個圈圈。
```javascript 
function game(){
  var times = 5
  var gameLog = []
  for (var x = 0 ; x < 5 ; x++){
    times -= 1
    gameLog.push(`第 ${x+1} 次丟出圈圈剩餘 ${times} 個`)
  }
  return gameLog
}
var personA = game()
console.log(personA)
// ["第 1 次丟出圈圈剩餘 4 個", 
// "第 2 次丟出圈圈剩餘 3 個",
// "第 3 次丟出圈圈剩餘 2 個", 
// "第 4 次丟出圈圈剩餘 1 個", 
// "第 5 次丟出圈圈剩餘 0 個"]
```

### 閉包的私有方法
讓我們再度強化機器人的可用性，基礎次數五次，每丟一次就記錄一次到陣列中，利用閉包的原理，將函式 return一個物件。
但是，這台機器人卻有個地方無法正常顯示，該怎麼修復它呢?
```javascript 
function game(){
  var times = 5
  var gameLog = []
  
  return {
    log: gameLog, 
    nowTimes: `目前剩餘次數為 ${times} 次`,
    addTimes: function(number){
      times += number
      return `目前剩餘次數為 ${times} 次`
    },
    start: function(){
      for (var x = 0 ; x < 5 ; x++){
        times -= 1
        gameLog.push(`第 ${x+1} 次丟出圈圈剩餘 ${times} 個`)
      }
      return '開始自動執行遊戲'
    }
  }
}

var personA = game()

console.log(personA.addTimes(25)) // 30
console.log(personA.nowTimes) // 卻還是顯示 5次
```

函式 `personA` return的物件，其包含許多函式與變數，而單純變數值卻無法修改? 我們來檢查看看，
```javascript 
// 節錄 personA片段
console.log(personA) 
// Object {
//   nowTimes: `目前剩餘次數為 5 次`,
//   addTimes: function(number){
//     times += number
//     return `目前剩餘次數為 ${times} 次`
//   },
// }
```

可以看到有的直接變成字串，而待在函式裡的變數則維持變數的樣子，讓我們再回想閉包的原理，若該函式內的變數有需要用到，則記憶體空間不會被釋放掉，也就是說，屬性值為單純的變數，被使用後就釋放掉了，待在函式內的則不會。那麼，我們來修正機器人，將 `addTimes` 這個屬型值改為函式，這樣就能正確顯示了。

```javascript 
function game(){
  var times = 5
  var gameLog = []
  
  return {
    log: gameLog, 
    nowTimes: function(){
      return `目前剩餘次數為 ${times} 次` 
    },
    addTimes: function(number){
      times += number
      return `目前剩餘次數為 ${times} 次`
    },
    start: function(){
      for (var x = 0 ; x < 5 ; x++){
        times -= 1
        gameLog.push(`第 ${x+1} 次丟出圈圈剩餘 ${times} 個`)
      }
      return '開始自動執行遊戲'
    }
  }
}

var personA = game()

console.log(personA.addTimes(25)) // 30
console.log(personA.nowTimes()) // 30
```

## 參考來源
> 1. [Huli - 所有的函式都是閉包：談 JS 中的作用域與 Closure](https://blog.techbridge.cc/2018/12/08/javascript-closure/)
> 2. [OneJar - 你不可不知的 JavaScript 二三事#Day5：湯姆克魯斯與唐家霸王槍—變數的作用域(Scope) (1)](https://ithelp.ithome.com.tw/articles/10203387)
