# 第五章 大師級函式:閉包與範圍
###### tags: `好想工作室`、`忍者讀書會`
## 5.1 瞭解閉包
#### ▌Scope(範圍、範疇、作用域)
**Scope就是變數可以被看見、被使用的範圍。**

* **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執行結束之後,這些記憶體就會跟著消失。 (如下圖流程所示)




* **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"`也會被消失才對,因此我們會預期第二個檢查失敗。

不過......

**(事實上)** 我們執行`innerFunction()`的時候,還是可以抓到`innerValue`這個變數。所以到底是什麼原因讓`innerValue`這個變數仍然是活的?答案就是閉包。
當我們在`outerfunction()`裡宣告`innerFunction()`時,做了兩件事情:
1. 定義了一個函式宣告`innerFunction()`
2. 建立了一個閉包:
* 閉包包含了函式定義`innerFunction()`以及在建立函式時存在於作用範圍內的所有參數。
* 如下圖,閉包就像一個保護用的氣泡,只要函式仍然存在,`innerFunction()`的閉包就會讓函式範圍內的變數一直保持在有效狀態下。

如果用程式碼來看他們之間的關係,就會如下圖。因此如果要用一句話來形容什麼是閉包?可以套用[techsith教學影片](https://youtu.be/71AtaJpJHw0?t=705)的一段話來形容:"Closures are nothing but FUNCTIONS WITH PRESERVED DATA",閉包就是函式包含該函式所保留的資料。
可以想像就是,當一個function使用到了自己function scope之外的變數,這個function包含使用到了的變數,就會形成一個閉包。

那這裡再稍微補充一下!
**(以下觀念來自[胡立的文章](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。

與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`變數,就像是一個真正的私有變數一樣。

### 5.2.2 伴隨回呼來使用閉包
閉包的另一個常見用途式處理回呼,也就是函式在稍後的某個時間點被呼叫。通常在這些函式裡,我們需要時常存取外部資料。
#### ▌Callback function 回呼函式
回呼函式就是把B函式當作A函式的參數,透過A函式來呼叫B函式,而這個被當作參數帶入的B函式將在「未來的某個時間點」被呼叫與執行(是一種非同步事件的一種方式)
舉例來說,要進入霍格華茲的巫師「被叫到名字」後要「上前戴上分類帽」,「戴上分類帽」後就會被「分學院」。

如果拿程式碼來說明就會像下面那樣:
```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()`,每個動畫都可以獲得私有變數「氣泡」。


****
## 5.3 使用執行背景空間追蹤程式執行
* 在JS中,函式是最基本的執行單元
* JS程式:
1. **全域程式**:放置在所有函式之外
2. **函式程式**:被包含在函式之中

* 當我們的程式被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)

****
## 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)
* 字彙環境主要基於巢狀程式,也就是一個程式碼結構能夠被包含另一個程式碼結構中(如下圖)

* 每一次程式碼被執行時,每一個程式碼結構都獲得相關的字彙環境。
* 字彙環境的規則(或稱scope的規則):
「外層 Scope 無法取用內層變數,但內層 Scope 可以取用外層變數」
接著就要來看JS引擎是如何追蹤這些變數?我們又可以從哪裡存去這些變數?這都是透過字彙環境所達成的。
### 5.4.2 巢狀程式與字彙環境

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

執行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環境的參照

****
## 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"
}
```
將得到以下錯誤訊息

/
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內的變數

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

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

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

從上圖可見這裡擁有三個字彙環境:
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");
```

從上圖可見這裡擁有三個字彙環境:
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函式嗎?

測試結果還是可以的!不過程式碼是逐行執行的,JS引擎是怎麼知道有一個check函式的存在?
#### ▌註冊識別項的過程
這是因為JS引擎使用了一點小手段,JS程式碼的執行其實分成了兩個階段:

其確切行為取決於變數類型、環境類型:
* 變數類型:let、var、函式宣告
* 環境類型:全域、函式、區塊
識別項在不同環境下的註冊過程:(JS程式碼的執行第一階段)

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`變數,因此這裡就產生了閉包

再來看看整體的部分,建構器函式是使用關鍵字new來呼叫的函式,因此每當我們呼叫一個建構器函式時,就等於建立了一個新的字彙環境,它會負責追蹤建構器函式的區域變數(以上述程式碼為例,即追蹤`feints`變數的新Ninja環境會被建立出來)
另外我們建立了兩個新函式:`getFeints`、`feint`,他們都具有對Ninja環境的參照。

當我們建立另一個Ninja物件:ninja2時,整個過程會再重複一遍。使用Ninja建構器所建立的每個物件都會得到屬於自己的方法(ninja1.getFeints()方法不同於ninja2.getFeints()方法)
他們把呼叫建構器函式時所定義的變數封閉起來,這些「私有」變數只能透過建構器函式中建立的物件方法(`getFeints`、`feint`方法)進行存取,而不能直接存取!

接著看看呼叫ninja2.getFeints()時到底發生哪些事?
1. 當呼叫函式時,會建立一個新的執行背景空間。所以這裡建立了一個getFeints執行背景空間,並將其推送到執行堆疊。

2. 同時也建立了一個getFeints字彙環境,用來追蹤在此函式中定義的變數。

3. getFeints字彙環境會取得建立getFeints函式時所屬的環境,也就是建立ninja2物件時有效的Ninja環境作為其外部環境。

4. 現在常是取得feints變數值,會先查詢當前活動中的getFeints字彙環境,由於我們沒有在getFeints函式中定義任何變數,所以這個字彙環境是空的。

5. 接下來會往當前字彙環境的外部環境搜尋,也就是Ninja環境,我們可以在這個字彙環境中找到feints變數,就搜尋完成了。

### 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中沒有物件是私有變數的,但是我們透過物件方法來建立閉包,來作為替代方案(做出類似私有物件的變數)。

### 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函式,所以就建立了兩個字彙環境)

2. 上述新建立的兩個字彙環境,都追蹤了動畫一組重要的變數:elementId、elem(進行動畫處理的DOM元素)、tick(目前的片刻數)、timer(執行動畫的計時器ID值)

3. 此例中,瀏覽器會讓setInterval裡的回呼函式一直持續著,直到我們呼叫clearInterval函式。
4. 到了指定的間隔時間,瀏覽器會呼叫對應的回呼函式,並且藉著閉包來存取在建立回呼時所定義的變數
---> 藉由建立多個閉包,便能夠一次做出許多事情
---> 每當有計時器到期,回呼函式會喚醒建立時所在的環境
---> 每次回呼的閉包都會自動追蹤自己所擁有的變數

## 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)