---
# System prepended metadata

title: 第五章 大師級函式：閉包與範圍
tags: [忍者讀書會, 好想工作室]

---

# 第五章 大師級函式：閉包與範圍
###### tags: `好想工作室`、`忍者讀書會`

## 5.1 瞭解閉包
#### ▌Scope（範圍、範疇、作用域）
**Scope就是變數可以被看見、被使用的範圍。**

![](https://i.imgur.com/NYaWr5W.jpg)


* **Global Scope**
    全域作用域，變數不在function或block裡面，而是在全域中宣告，因此任何地方都能使用該變數。
    ```javascript!
    var totalEggs = 6
    function collectEggs(){
        console.log(totalEggs) //6
    }

    collectEggs()
    ```
* **Local Scope** 
    又分為兩種區域作用域：Function Scope、Block Scope(ES6)
    * **Function Scope**
        function有自己的作用域，所以在function裡面宣告變數，在function之外的地方都沒辦法使用。
        ```javascript!
        function collectEggs(){
            var totalEggs = 6
        }

        console.log(totalEggs) //totalEggs is not defined
        ```
        即便先執行了這個function，外面仍然無法取得function內部宣告的變數。
        ```javascript!
        function collectEggs(){
            var totalEggs = 6
        }
    
        collectEggs()
        console.log(totalEggs) //totalEggs is not defined
        ```
        這是因為function執行時會進入call stack，這裡的變數totalEggs會存到stack memory中（stack memory是用來保存由函式所產生的暫時性變數，或稱區域變數），stack memory使用的是暫存性的記憶體空間，所以當function執行結束之後，這些記憶體就會跟著消失。 （如下圖流程所示）
        
        ![](https://i.imgur.com/HZWAVEK.jpg)
        ![](https://i.imgur.com/4BBKABQ.jpg)
        ![](https://i.imgur.com/GBPza2H.jpg)
        ![](https://i.imgur.com/TuHxpIC.jpg)
        
    * **Block Scope(ES6)**
        ES6之前，只有function可以建立scope。
        ES6之後，出現了let、const宣告，使得我們可以用「大括號」建立scope。（大括號範圍就是block，例如if判斷式、while或for迴圈）
        


#### ▌程式列表5.1 一個簡單的閉包
```javascript!
var outerValue = "ninja";
function outerFunction() {
  assert(outerValue === "ninja","I can see the ninja.");
}
outerFunction();
```
如果用上面scope的概念來看，會覺得`outerValue === "ninja"`為true，這是再正常不過的事情，因為`outerValue`是在全域中宣告，所以任何地方都可以使用到這個變數。

但事實上`outerFunction()`能看到、使用自己function scope之外的變數，其實就是在建立一個閉包。

我們在看下個複雜點的例子。



#### ▌程式列表5.2 另一個閉包的例子
```javascript!
var outerValue = "samurai";
var later;

function outerFunction() {
  var innerValue = "ninja";

  function innerFunction() {
    assert(outerValue === "samurai", "I can see the samurai.");
    assert(innerValue === "ninja", "I can see the ninja.");
  }

  later = innerFunction;
}

outerFunction();
later();
```

**（我們所想的）** 就像前面提到的，當執行一個function，變數若是在function scope裡面建立的，會被存放到stack memory，一個暫存性的記憶體空間。

所以說當`outerfunction()`一執行結束，照理說`innerValue = "ninja"`也會被消失才對，因此我們會預期第二個檢查失敗。

![](https://i.imgur.com/1AU52rK.jpg)

不過......

![](https://i.imgur.com/t5SFHPu.png)


**（事實上）** 我們執行`innerFunction()`的時候，還是可以抓到`innerValue`這個變數。所以到底是什麼原因讓`innerValue`這個變數仍然是活的？答案就是閉包。

當我們在`outerfunction()`裡宣告`innerFunction()`時，做了兩件事情：
1. 定義了一個函式宣告`innerFunction()`
2. 建立了一個閉包：
    * 閉包包含了函式定義`innerFunction()`以及在建立函式時存在於作用範圍內的所有參數。
    * 如下圖，閉包就像一個保護用的氣泡，只要函式仍然存在，`innerFunction()`的閉包就會讓函式範圍內的變數一直保持在有效狀態下。

![](https://i.imgur.com/CHabfzV.jpg)

如果用程式碼來看他們之間的關係，就會如下圖。因此如果要用一句話來形容什麼是閉包？可以套用[techsith教學影片](https://youtu.be/71AtaJpJHw0?t=705)的一段話來形容："Closures are nothing but FUNCTIONS WITH PRESERVED DATA"，閉包就是函式包含該函式所保留的資料。

可以想像就是，當一個function使用到了自己function scope之外的變數，這個function包含使用到了的變數，就會形成一個閉包。

![](https://i.imgur.com/9ic0mem.jpg)


那這裡再稍微補充一下！

**（以下觀念來自[胡立的文章](https://blog.huli.tw/2018/12/08/javascript-closure/)）**
上面提到「function使用到了自己function scope之外的變數」，其實這樣的變數有一個特殊的名字。

對於`innerFunction()`這個function來說，`innerValue`或者`outerValue`都不是它自己的變數，而這種不在自己作用域中，也不是被當成參數傳進來的變數，就可以被稱作「free variable（自由變數）」。

所以對`innerFunction()`來說，`innerValue`以及`outerValue`都是自由變數。

另外，一個function在自己的作用域中找不到變數，就會往外面一層的作用域尋找，如果還是找不到，就會再往上一層直到找到為止（如果到全域還是找不到就會拋出錯誤），這個過程就會構成一個「Scope Chain（作用域鏈）」。

**（以下觀念來自[Fireship的影片](https://www.youtube.com/watch?v=vKJpN5FAeF4&list=PLJ1djWumFFcno2X_dXvfjupsdpHxzc55k&index=2&t=122s)）**
上面有提到說「閉包就會讓函式範圍內的變數一直保持在有效狀態下」，這是因為當closure建立，會將那些包含在閉包內的變數，給存放到heap memory。

![](https://i.imgur.com/x7uJJGa.jpg)

與stack memory不同，stack memory是function執行結束就會把記憶體空間給釋放，而heap memory則是function執行結束後，裏頭存放的東西仍然存在，直到我們去做清除的動作。（至於如何清除，不同語言有不同的方法，詳情可見[Stack vs. Heap](https://medium.com/joe-tsai/stack-vs-heap-b4bd500667cd)）

所以說closure的缺點就在這邊，它會相對地比較佔記憶體空間。

****
## 5.2 開始使用閉包
在5.2章節中，我們會來看看如何在JavaScript中使用閉包。
（分別為模擬私有變數、處理回呼）
### 5.2.1 模擬私有變數
JavaScript本身不支援私有變數，但可以藉由閉包來做出類似的功能。
#### ▌程式列表5.3 使用閉包來模擬私有變數
```javascript!
function Ninja() {
  var feints = 0;
  this.getFeints = function(){
    return feints;
  };
  this.feint = function(){
    feints++;
  };
 }

 var ninja1 = new Ninja();
 ninja1.feint();

 assert(ninja1.feints === undefined,
        "And the private data is inaccessible to us.");
 assert(ninja1.getFeints() === 1,
       "We're able to access the internal feint count.");


 var ninja2 = new Ninja();
 assert(ninja2.getFeints() === 0,
        "The second ninja object gets it’s own feints variable.");
```

**分析**
這裡建立了一個建構器函式：Ninja。
```javascript!
function Ninja() {
  var feints = 0;
  this.getFeints = function(){
    return feints;
  };
  this.feint = function(){
    feints++;
  };
 }
```
* `feints` 變數：，用來紀錄狀態。
* `getFeints` 方法：因為變數`feints`在`Ninja()`function裡面被宣告，所以只能在這個constructor function裡面被使用、存取， `getFeints` 方法便是為了允許範圍之外的程式碼可以存取到這個變數的值，而有了`getFeints`讀值器的方法。
* `feint` 方法：用來控制變數的值，在本例中，此方法可以增加`feints`的值。

接著我們便可以新建立ninja1物件，並且在此物件上呼叫`feint`方法，呼叫`feint`方法時，它會增加ninja1的`feints`值。
```javascript!
 var ninja1 = new Ninja();
 ninja1.feint();

 assert(ninja1.feints === undefined,
        "And the private data is inaccessible to us.");
 assert(ninja1.getFeints() === 1,
       "We're able to access the internal feint count.");
```
接著從測試中的結果可知：
1. `ninja1.feints === undefined`為true，代表我們無法從外部存取函式內部的變數。
2. `ninja1.getFeints() === 1`為true，代表雖然我們無法從外部取值，但還是有辦法更改`feints`這個私有變數的值。

而最後，我們再使用Ninja建構器建立一個ninja2物件。
```javascript!
 var ninja2 = new Ninja();
 assert(ninja2.getFeints() === 0,
        "The second ninja object gets it’s own feints variable.");
```
可以從測試中得知新建立的ninja2物件，它有屬於自己的feints變數。

**結論**
利用閉包可以讓忍者的狀態被保持在一個方法中，而無法被使用者直接存取。因為閉包讓內部方法可以存取變數，但是位於建構器函式外部的程式碼則不行。

這個方法讓`feints`變數，就像是一個真正的私有變數一樣。

![](https://i.imgur.com/FBbDybx.jpg)

### 5.2.2 伴隨回呼來使用閉包
閉包的另一個常見用途式處理回呼，也就是函式在稍後的某個時間點被呼叫。通常在這些函式裡，我們需要時常存取外部資料。

#### ▌Callback function 回呼函式
回呼函式就是把B函式當作A函式的參數，透過A函式來呼叫B函式，而這個被當作參數帶入的B函式將在「未來的某個時間點」被呼叫與執行（是一種非同步事件的一種方式）

舉例來說，要進入霍格華茲的巫師「被叫到名字」後要「上前戴上分類帽」，「戴上分類帽」後就會被「分學院」。

![](https://i.imgur.com/aeFuSqH.jpg)

如果拿程式碼來說明就會像下面那樣：
```javascript=
wizard.addEventListener("被叫到名字" ,function(){
    戴上分類帽;
    wizard.addEventListener("戴上分類帽" ,function(){
        分學院;
        })
    })
```
我們對巫師做事件監聽，當巫師被叫到名字後，才會執行事件監聽第二個參數中的function。所以`戴上分類帽`這個動作只會在滿足了`被叫到名字`這個條件才會被動地去執行，我們就可以說這是一個 Callback function。

Callback function常見例子有setTimeout()、setInterval()、DOM 的事件監聽、從資料庫或遠端伺服器請求資料等等。

接著忍者裡面的範例會使用setTimeout()來做說明。

#### ▌程式列表5.4 在回呼計時器裡使用閉包
```javascript!
function animateIt(elementId) {
  var elem = document.getElementById(elementId);
  var tick = 0;
  var timer = setInterval(function(){
    if (tick < 100) {
      elem.style.left = elem.style.top = tick + "px";
      tick++;
    }
    else {
      clearInterval(timer);
    }
  }, 10);
}
animateIt("box1");
```
上面範例利用了closure的方式達到製作動畫的效果，它使用了一個匿名函式做為setInterval的參數，來完成目標div元素的動畫效果。這個匿名函式藉由閉包來存取三個變數：elem、tick、timer。

* elem：ID為"box1"的DOM元素參照
* tick：片刻計數器
* timer：計時器setInterval的參照

接著，來修改一下上述範例：
* [第一個codepen範例](https://codepen.io/fgfjgror/pen/GRGjvyM?editors=1011)：
    現在如果將這三個變數都移出`animateIt()`函式外面，讓這三個變數變成全域變數，動畫仍然可以順利執行的。

    代表說我們可以不必利用closure，就能做到一模一樣的效果，所以為什麼還要建立閉包？這就要看到第二個範例了。

* [第二個codepen範例](https://codepen.io/fgfjgror/pen/poKErBX)
        如果我們將變數儲存在全域範圍中，我們每增加一個動畫，就要為該動畫設置三個變數，因此如果我們藉著函式內部定義變數，在計時器進行回呼時透過閉包讓那些變數可以被使用，每當我們呼叫`animateIt()`，每個動畫都可以獲得私有變數「氣泡」。
        ![](https://i.imgur.com/DwQy2QV.jpg)
        ![](https://i.imgur.com/wIq7DEe.jpg)


****

## 5.3 使用執行背景空間追蹤程式執行
* 在JS中，函式是最基本的執行單元
* JS程式：
    1.  **全域程式**：放置在所有函式之外
    2.  **函式程式**：被包含在函式之中
    ![](https://i.imgur.com/oHgcVVf.jpg)
    * 當我們的程式被JS引擎執行的時候，每個敘述句都在一個特定的++執行背景空間++中執行。
* ++執行背景空間++：
    1. **全域執行背景空間**：只會有一個全域執行背景空間，它是在我們的JS程式開始執行時建立的。
    2. **函式執行背景空間**：每次呼叫函式就會建立一個新的函式執行背景。
:::warning
* 注意：函式背景空間 vs. 函式執行背景空間

    | 名稱             | 解釋                                                   |
    | ---------------- | ------------------------------------------------------ |
    | 函式背景空間     | 函式被呼叫時所屬的物件，可以使用this關鍵字來存取它     |
    | 函式執行背景空間 | 這是JS引擎所使用的一種內部概念，並用它來追蹤函式的執行 |
:::
* JS是單執行緒的執行模型（Single Threaded），也就是說他一次只能執行一段程式碼
* 每當一個函式被呼叫時，都必須停止當前的執行背景空間，然後再建立一個新的函式執行背景空間，當函式的任務完成後，它的函式執行背景空間通常就會被丟棄，並恢復到原本呼叫者的執行背景空間。
    ---> 所有的執行背景空間都需要進行追蹤（無論執行中、等待中）
    ---> 利用堆疊（stack）追蹤，它被稱為執行背景空間堆疊（或稱呼叫堆疊，也就是在筆記5.1提到的call stack）
    
    直接來看看程式碼範例。
    
#### ▌程式列表5.5 建立執行背景空間
```javascript!
function skulk(ninja) {
  report(ninja + " skulking");
}

function report(message) {
  console.log(message);
}

skulk("Kuma");
skulk("Yoshi");
```
上述範例在執行背景空間的堆疊行為如下圖，但是用看的，不如用chrome的dev tool來看看它實際的行為。（這裡直接用dev tool去操作程式列表5.5）

![](https://i.imgur.com/c3BhMUc.jpg)

****

## 5.4 使用字彙環境來追蹤識別項
* 字彙環境（Lexical Scope）：JS引擎的一個內部結構，用於追蹤從++識別項++到特定變數的對應（mapping）
    * ++識別項++（identifier）：（參考[Chidre'sTechTutorials影片](https://www.youtube.com/watch?v=UzKMBLWeQ-A)）
        舉例來說：變數的名稱、常數的名稱、陣列的名稱、函式的名稱、物件的名稱等等，都可以叫做識別項。
:::warning
* 注意：
字彙環境是JS作用範圍界定機制的內部實作，而人們經常會在口語上將他們稱為Scope（範圍、範疇、作用域）
:::
* 字彙環境（或者就叫它scope）與JS程式碼的特定結構相關聯，這些特定結構包含：一個函式、一段程式區塊、catch區塊，他們都可以有個別的識別項對應。
:::warning
* 注意：
    * ES6前：字彙環境只可以與函式相關聯（所以只有function scope）
    * ES6後：有了block scope的存在
:::

### 5.4.1 巢狀程式（code nesting）
* 字彙環境主要基於巢狀程式，也就是一個程式碼結構能夠被包含另一個程式碼結構中（如下圖）

![](https://i.imgur.com/QqxDNfa.jpg)

* 每一次程式碼被執行時，每一個程式碼結構都獲得相關的字彙環境。
* 字彙環境的規則（或稱scope的規則）：
    「外層 Scope 無法取用內層變數，但內層 Scope 可以取用外層變數」

接著就要來看JS引擎是如何追蹤這些變數？我們又可以從哪裡存去這些變數？這都是透過字彙環境所達成的。

### 5.4.2 巢狀程式與字彙環境
![](https://i.imgur.com/Y6YLMwp.jpg)

* report函式由skulk函式呼叫
* skulk函式由全域環境呼叫

每個執行背景空間具有與其相關聯的字彙環境，它包含了在該背景空間中所定義的所有識別項對應，例如：
* 全域環境：保留了識別項ninja、skulk的對應
* skulk環境：保留了識別項action、report的對應
* report環境：保留了識別項intro的對應

![](https://i.imgur.com/Uo68i1Y.jpg)

執行report函式時，JS引擎解析識別項的步驟：
* 尋找intro：
    1. 檢查report環境 -> <font color="green">有</font>
* 尋找action：
    1. 檢查report環境 -> <font color="red">無</font>
    2. 檢查report的外部環境：skulk
        檢查skulk環境 -> <font color="green">有</font>
* 尋找ninja：
    1. 檢查report環境 -> <font color="red">無</font>
    2. 檢查report的外部環境：skulk
        檢查skulk環境 -> <font color="red">無</font>
    4. 檢查skulk的外部環境：global
        檢查global環境 -> <font color="red">無</font>
        
除了存取對應的字彙環境中定義的識別項之外（例如report環境保留了識別項intro的對應），程式也常常存取在外部環境中定義的其他變數（例如report函式裡，存取了skulk的變數action、全域變數nunja）
---> 為了做到這一點，我們必須以某種方式來追蹤這些外部環境，而JS作法則是讓函式做為頭等物件。

每當建立一個函式時，指向該函式所屬字彙環境的參照，會儲存在名為[[Environment]]的內部屬性裡面（表示不能被直接存取或是操作）。
* skulk函式保持對全域環境的參照
* report函式保持對skulk環境的參照

![](https://i.imgur.com/CFHlQtJ.jpg)

****

## 5.5 瞭解JavaScript的變數類型
* 在JS中，有三種關鍵字可用來定義變數：
    * var
    * let
    * const
* 他們在兩個方面有所不同：
    * 可變性：【const】 vs. 【var、let】
    * 字彙環境的關係：【var】 vs. 【const、let】

### 5.5.1 變數可變性
* 如果用可變性來區分變數宣告的關鍵字，我們會這樣分成兩組：【const】 vs. 【var、let】
    * 【const】：所有使用const定義的變數都是不可變的，意味著他們的值只能設置一次
    * 【var、let】：典型的普通變數，其值可以根據需求改變許多次

這一個章節主要會來瞭解const變數的運作方式以及行為。

#### ▌const變數
* const變數類似於一般變數，但是它在宣告時就需要提供一個初始值，而在那之後，我們就不能指派新的值給它了。

* const變數常用於兩種目的：
    * 指定一些不應重新指派的變數（在本書中使用const，多半是這個目的）
    * 參照到一個固定值
        例如，用名稱來表示一個浪人團最多可以由幾位浪人組成：MAX_RONIN_count
        
#### ▌程式列表5.6 const變數的行為
```javascript!
const firstConst = "samurai";
assert(firstConst === "samurai", "firstConst is a samurai");

try {
  firstConst = "ninja";
  fail("Shouldn't be here");
} catch (e) {
  pass("An exception has occured");
}

assert(firstConst === "samurai", "firstConst is still a samurai!");

const secondConst = {};

secondConst.weapon = "wakizashi";
assert(secondConst.weapon === "wakizashi", "We can add new properties");

const thirdConst = [];
assert(thirdConst.length === 0, "No items in our array");

thirdConst.push("Yoshi");

assert(thirdConst.length === 1, "The array has changed");
```
**分析**
1-1. 先來定義一個const變數，並確認有指派一個值給它
```javascript!
const firstConst = "samurai";
assert(firstConst === "samurai", "firstConst is a samurai");
```

1-2. 若企圖指派一個新值給const變數，就會發生異常
```javascript!
try {
  firstConst = "ninja";
  fail("Shouldn't be here");
} catch (e) {
  pass("An exception has occured");
}
```
/
2-1. 建立一個新的constant變數，並指派一個物件給它
```javascript!
const secondConst = {};
```

2-2. 我們無法指派一個全新的物件給secondConst變數，但可以修改它
```javascript!
secondConst.weapon = "wakizashi";
assert(secondConst.weapon === "wakizashi", "We can add new properties");
```

若在此處指派一個全新的物件給secondConst變數（如下方所示）
```javascript!
const secondConst = {};
secondConst = {
  weapon: "wakizashi"
}
```
將得到以下錯誤訊息
![](https://i.imgur.com/c583d47.png)


/
3-1. 上述規則也適用於陣列。先建立一個新的constant變數，並指派一個陣列給它，且測試確認thirdConst為一個空陣列。
```javascript!
const thirdConst = [];
assert(thirdConst.length === 0, "No items in our array");
```

3-2. 一樣無法指派全新的陣列給thirdConst變數，但可以修改它
```javascript!
thirdConst.push("Yoshi");
assert(thirdConst.length === 1, "The array has changed");
```

**結論**
1. const變數的值只能在初始化時設置，不能在之後指派一個全新的值給它
2. 可以修改現有的值，只是不能完全複寫它

### 5.5.2 用來定義變數的關鍵字和字彙環境
* 如果用根據他們與字彙環境的關係（i.e.他們的scope）來區分變數宣告的關鍵字，我們會這樣分成兩組：【var】 vs. 【const、let】

#### ▌使用var關鍵字
使用var關鍵字時，變數是定義在最鄰近的函式或全域字彙環境中（程式區塊block scope會被忽略！畢竟它是ES6後才出現的，比var還晚出生）

#### ▌程式列表5.7 使用var關鍵字
```javascript!
var globalNinja = "Yoshi";

function reportActivity(){
  var functionActivity = "jumping";

  for(var i = 1; i < 3; i++) {
      var forMessage = globalNinja + " " + functionActivity;
      assert(forMessage === "Yoshi jumping",
             "Yoshi is jumping within the for block");
      assert(i, "Current loop counter:" + i);
  }

  assert(i === 3 && forMessage === "Yoshi jumping",
        "Loop variables accessible outside of the loop");
  }

reportActivity();
assert(typeof functionActivity === "undefined"
    && typeof i === "undefined" && typeof forMessage === "undefined",
    "We cannot see function variables outside of a function");
```
**分析**
1-1. 使用var來定義一個全域變數globalNinja為Yoshi
```javascript!
var globalNinja = "Yoshi";
```

2-1. 在函式內使用var定義一個區域變數functionActivity為jumping
2-2. 在for loop裡使用var來定義兩個變數i、forMessage
2-3. 在for loop中測試我們可以存取到外面的區塊變數、函式變數、全域變數
2-4. 但在for loop外，也可以存取到for loop內的變數
![](https://i.imgur.com/Vs0EsYt.jpg)

3-1. 在函式之外無法存取函式裡面的變數
```javascript!
assert(typeof functionActivity === "undefined"
    && typeof i === "undefined" && typeof forMessage === "undefined",
    "We cannot see function variables outside of a function");
```
![](https://i.imgur.com/Ce82BWm.jpg)

而上述程式碼中，有一段是JS的奇怪之處，就是在2-4測試的部分。我們竟然可以在區塊之外，繼續存取區塊內所定義的變數。
---> 這是因為使用關鍵字var宣告的變數，總是會「註冊在最鄰近的函式或全域環境中」，而無視程式區塊的結構。

![](https://i.imgur.com/qbPM4jr.jpg)

block scope被忽視後，var定義的變數就會去找「最鄰近的函式或全域環境」，以上述程式碼為例，即reportActivity環境中（因為它是最鄰近的函式環境）

![](https://i.imgur.com/fcozIPe.jpg)

從上圖可見這裡擁有三個字彙環境：
1. 全域環境：註冊globalNinja變數的環境（因為這是最鄰近的函式或是全域字彙環境）
2. reportActivity環境：
    * 呼叫reportActivity函式時建立的
    * 環境中包含的變數：（因為這是他們最鄰近的函式）
        * functionActivity
        * i
        * forMessage
3. for區塊：它是空的（因為用var定義的變數會忽略程式區塊）

由於這種行為非常奇怪，所以在ES6版本的JS提供了兩個新的變數宣告關鍵字。

#### ▌使用let和const來指定區塊範圍內的變數
使用let和const關鍵字，他們會把變數定義在最接近的字彙環境中（可以是區塊環境、迴圈環境、函式環境、全域環境）。

#### ▌程式列表5.8 使用關鍵字let和const
```javascript!
const globalNinja = "Yoshi";

function reportActivity(){
  const functionActivity = "jumping";

  for(let i = 1; i < 3; i++) {
      let forMessage = globalNinja + " " + functionActivity;
      assert(forMessage === "Yoshi jumping",
             "Yoshi is jumping within the for block");
      assert(i, "Current loop counter:" + i);
  }

  assert(typeof i === "undefined" && typeof forMessage === "undefined",
        "Loop variables not accessible outside the loop");
  }

reportActivity();
assert(typeof functionActivity === "undefined"
    && typeof i === "undefined" && typeof forMessage === "undefined",
    "We cannot see function variables outside of a function");
```
![](https://i.imgur.com/YbWoyyV.jpg)

從上圖可見這裡擁有三個字彙環境：
1. 全域環境：globalNinja變數定義在全域環境中
3. reportActivity環境：functionActivity變數定義在reportActivity環境中
4. for區塊：i變數、forMessage變數定義在for區塊中

這是因為使用let和const關鍵字，變數會被定義在最鄰近的環境中。

### 5.5.3 在字彙環境中註冊識別項
首先我們知道JS是以滿直接的方式逐行執行的，再來看看以下例子：
```javascript!
const firstRonin = "Kiyokawa";
check(firstRonin);
function check(ronin) {
  assert(ronin === "Kiyokawa", "The ronin was checked!");
}
```
如果說程式碼是逐行執行，那還沒執行到定義函式的地方，能夠呼叫到check函式嗎？

![](https://i.imgur.com/jE77Yt6.png)

測試結果還是可以的！不過程式碼是逐行執行的，JS引擎是怎麼知道有一個check函式的存在？

#### ▌註冊識別項的過程
這是因為JS引擎使用了一點小手段，JS程式碼的執行其實分成了兩個階段：
![](https://i.imgur.com/UV25Xxv.jpg)

其確切行為取決於變數類型、環境類型：
* 變數類型：let、var、函式宣告
* 環境類型：全域、函式、區塊

識別項在不同環境下的註冊過程：（JS程式碼的執行第一階段）
![](https://i.imgur.com/Sgxo0oO.jpg)
1. 第一步驟：如果我們建立一個函式環境，伴隨著的「函式參數」、「其引數值的隱含式arguments識別項」，會被建立出來。
2. 第二步驟：如果我們建立的式一個全域環境或是函式環境，會先掃描目前的程式碼，找出所有的「函式宣告」（不包含函式表達式、箭頭函式），針對發現的每個函式宣告，都建立一個新的函式，並將它綁定到該環境中的同名識別項上。
3. 第三步驟：找出「變數宣告」。

#### ▌在函式宣告前呼叫它
讓JS如此好用的一項特色，是函式定義的順序無關緊要。
#### ▌程式列表5.9 在函式宣告前對它進行存取
```javascript!
assert(typeof fun === "function",
      "fun is a function even though its definition isn’t reached yet!");

assert(typeof myFunExp === "undefined",
      "But we cannot access function expressions");

assert(typeof myLamda === "undefined",
       "Nor lambda functions");

function fun(){}

var myFunExpr = function(){};
var myLambda = (x) => x;
```
先別管測試，用簡單一點的方式來看上面的程式碼
```javascript!
typeof fun //function
typeof myFunExp //undefined
typeof myLamda //undefined
function fun(){} //在執行JS程式碼之前，就會先註冊已宣告的函式
var myFunExpr = function(){}; //myFunExpr指向一個函式表達式
var myLambda = (x) => x; //myLambda指向一個箭頭函式
```
* fun函式在執行JS程式碼之前，就已經先註冊了（已存在），因此能夠在不同地方呼叫它
* 函式表達式、箭頭函式不在此過程中，他們是在程式執行到其定義位置時才建立的，所以無法在他們建立之前存取這兩種函式。

#### ▌複寫函式
函式識別項可能會被複寫。
#### ▌程式列表5.10 複寫函式識別項
```javascript!
assert(typeof fun === "function", "We access the function");

var fun = 3;

assert(typeof fun === "number", "Now we access the number");

function fun(){}

assert(typeof fun == "number", "Still a number");
```
一樣先不管測試，比較方便觀察
```javascript!
typeof fun //function
var fun = 3 //執行JS之前即註冊（註冊識別項的第三步驟：處理變數宣告）
typeof fun //number
function fun(){} //執行JS之前即註冊（註冊識別項的第二步驟：處理函式宣告）
typeof fun //number
```
* 在此範例中，變數宣告和函式宣告都具有相同的名稱：fun
* JS程式碼執行前，發生兩件事情：
    1. `function fun(){}`：在註冊識別項的第二步驟，識別項fun以函式宣告被註冊。
    2. `var fun = 3`：在註冊識別項的第三步驟，開始處理變數宣告，此時它會將數字3指派給識別項fun，這讓我們的識別項fun失去了對函式的參照，現在識別項fun變成了一個數字。

以上如果用簡化的觀點來看，就是常看到的一個術語「提升」（hoisting），但在技術上來說，變數和函式宣告並不會「移動」到任何地方，他們是在任何程式碼執行之前，在字彙環境中就先被取得並進行註冊。

****

## 5.6 探索閉包的運作方式
### 5.6.1 重新檢視如何使用閉包來模擬私有變數
#### ▌程式列表5.11 使用閉包來產生近乎私有的變數
```javascript!
function Ninja() {
  var feints = 0;
  this.getFeints = function(){
    return feints;
  };
  this.feint = function(){
    feints++;
  };
}
var ninja1 = new Ninja();
ninja1.feint();

var ninja2 = new Ninja();
```
回顧一下5.2章節，`feint`函式要存取`feints`變數，因此這裡就產生了閉包
![](https://i.imgur.com/FBbDybx.jpg)

再來看看整體的部分，建構器函式是使用關鍵字new來呼叫的函式，因此每當我們呼叫一個建構器函式時，就等於建立了一個新的字彙環境，它會負責追蹤建構器函式的區域變數（以上述程式碼為例，即追蹤`feints`變數的新Ninja環境會被建立出來）

另外我們建立了兩個新函式：`getFeints`、`feint`，他們都具有對Ninja環境的參照。
![](https://i.imgur.com/0FOu7X3.jpg)

當我們建立另一個Ninja物件：ninja2時，整個過程會再重複一遍。使用Ninja建構器所建立的每個物件都會得到屬於自己的方法（ninja1.getFeints()方法不同於ninja2.getFeints()方法）

他們把呼叫建構器函式時所定義的變數封閉起來，這些「私有」變數只能透過建構器函式中建立的物件方法（`getFeints`、`feint`方法）進行存取，而不能直接存取！
![](https://i.imgur.com/BKl0tOx.jpg)

接著看看呼叫ninja2.getFeints()時到底發生哪些事？
1. 當呼叫函式時，會建立一個新的執行背景空間。所以這裡建立了一個getFeints執行背景空間，並將其推送到執行堆疊。
![](https://i.imgur.com/0M6OHhy.jpg)


2. 同時也建立了一個getFeints字彙環境，用來追蹤在此函式中定義的變數。
![](https://i.imgur.com/sfAQphU.jpg)

3. getFeints字彙環境會取得建立getFeints函式時所屬的環境，也就是建立ninja2物件時有效的Ninja環境作為其外部環境。
![](https://i.imgur.com/ZXZH8La.jpg)

4. 現在常是取得feints變數值，會先查詢當前活動中的getFeints字彙環境，由於我們沒有在getFeints函式中定義任何變數，所以這個字彙環境是空的。
![](https://i.imgur.com/GrqA2bH.jpg)

5. 接下來會往當前字彙環境的外部環境搜尋，也就是Ninja環境，我們可以在這個字彙環境中找到feints變數，就搜尋完成了。
![](https://i.imgur.com/ZrOnS4x.jpg)

### 5.6.2 留意私有變數
在JS中，可以把在一個物件上建立的屬性再指派給另一個物件。
#### ▌程式列表5.12 私有變數要透過函式來存取，而不是透過物件！
```javascript!
function Ninja() {
  var feints = 0;
  this.getFeints = function(){
    return feints;
  };
  this.feint = function(){
    feints++;
  };
}
var ninja1 = new Ninja();
ninja1.feint();

var imposter = {};
imposter.getFeints = ninja1.getFeints;

assert(imposter.getFeints () === 1,
      "The imposter has access to the feints variable!");
```
這裡利用`imposter.getFeints = ninja1.getFeints;`，將ninja1.getFeintsz方法指派給一個全新的imposter物件，而且當我們再imposter物件上呼叫getFeints函式時，是可以存取到ninja1的變數feints值。

這個例子說明了JS中沒有物件是私有變數的，但是我們透過物件方法來建立閉包，來作為替代方案（做出類似私有物件的變數）。
![](https://i.imgur.com/BUqkjdR.jpg)

### 5.6.3 重新檢視閉包和回呼範例
#### ▌程式列表5.13 在timer回呼函式中使用閉包
```javascript!
function animateIt(elementId) {
  var elem = document.getElementById(elementId);
  var tick = 0;
  var timer = setInterval(function(){
    if (tick < 100) {
      elem.style.left = elem.style.top = tick + "px";
      tick++;
    }
    else {
      clearInterval(timer);
      assert(tick === 100,
             "Tick accessed via a closure.");
      assert(elem,
             "Element also accessed via a closure.");
      assert(timer,
             "Timer reference also obtained via a closure." );
    }
  }, 10);
}
animateIt("box1");
animateIt("box2");
```
1. 每次呼叫animateIt函式時，都會建立一個新的函式字彙環境。（這邊有兩個animateIt函式，所以就建立了兩個字彙環境）
![](https://i.imgur.com/zr44fxJ.jpg)

2. 上述新建立的兩個字彙環境，都追蹤了動畫一組重要的變數：elementId、elem（進行動畫處理的DOM元素）、tick（目前的片刻數）、timer（執行動畫的計時器ID值）
![](https://i.imgur.com/OvRY8yn.jpg)

3. 此例中，瀏覽器會讓setInterval裡的回呼函式一直持續著，直到我們呼叫clearInterval函式。

4. 到了指定的間隔時間，瀏覽器會呼叫對應的回呼函式，並且藉著閉包來存取在建立回呼時所定義的變數
---> 藉由建立多個閉包，便能夠一次做出許多事情
---> 每當有計時器到期，回呼函式會喚醒建立時所在的環境
---> 每次回呼的閉包都會自動追蹤自己所擁有的變數
![](https://i.imgur.com/sJ8LNHZ.jpg)

## 5.8 習題


****        

## 參考資料
* [[CS] 堆疊和堆積（Stack Memory and Heap Memory）](https://pjchender.dev/computer-science/cs-stack-heap/)
* ==推== [Closures Explained in 100 Seconds // Tricky JavaScript Interview Prep](https://www.youtube.com/watch?v=vKJpN5FAeF4&list=PLJ1djWumFFcno2X_dXvfjupsdpHxzc55k&index=2&t=121s) 
* ==推== [Huli - 所有的函式都是閉包：談 JS 中的作用域與 Closure](https://blog.huli.tw/2018/12/08/javascript-closure/)
* [[JavaScript學習系列]什麼是JavaScript的Scope，弄懂Scope的規範，才能避免不必要的bug](https://rollerblade.tw/javascript-scope/)
* [Javascript Closure tutorial ( Closures Explained )](https://www.youtube.com/watch?v=71AtaJpJHw0)
* [ 9.6: JavaScript Closure - p5.js Tutorial](https://www.youtube.com/watch?v=-jysK0nlz7A&list=PLJ1djWumFFcno2X_dXvfjupsdpHxzc55k&index=1)