# Introduction

## 關於本書

* [2019 iT 邦幫忙鐵人賽傳送門](https://ithelp.ithome.com.tw/users/20112483/ironman/2016)
* 如果發現任何理解錯誤或筆誤，歡迎[告知指正](https://github.com/onejar99/something-about-javascript/issues)\~

## 文章目錄

* Day 1：[前言](/day01)
* Day 2：[資料型態的夢魘——動態型別加弱型別(1)](/day02)
* Day 3：[資料型態的夢魘——動態型別加弱型別(2)](/day03)
* Day 4：[動態型別加弱型別不是罪——怎麼 JavaScript 一摔就變成個印度阿三？](/day04)
* Day 5：[湯姆克魯斯與唐家霸王槍——變數的作用域(Scope) (1)](/day05)
* Day 6：[湯姆克魯斯與唐家霸王槍——變數的作用域(Scope) (2)](/day06)
* Day 7：[傳統 var 關鍵字的不足](/day07)
* Day 8：[var 掰掰 —— ES6 更嚴謹安全的 let 和 const](/day08)
* Day 9：[圖解變數作用域(Scope)](/day09)
* Day 10：[程式也懂電梯向上？ —— Hoisting](/day10)
* Day 11：[Strict Mode (嚴謹模式)](/day11)
* Day 12：[看 Strict Mode 如何施展「還我漂亮拳」(1)](/day12)
* Day 13：[看 Strict Mode 如何施展「還我漂亮拳」(2)](/day13)
* Day 14：[來挖挖恐龍骨 —— with 語法](/day14)
* Day 15：[this 關鍵字 (1)](/day15)
* Day 16：[this 關鍵字 (2)](/day16)
* Day 17：[this 關鍵字 (3)](/day17)
* Day 18：[this 關鍵字 (4)](/day18)
* Day 19：[函數定義 (Function Definition) 的 100 種寫法](/day19)
* Day 20：[ES6 的箭頭函數 (Arrow Functions)](/day20)
* Day 21：[箭頭函數 (Arrow Functions) 的 this 和你想的不一樣 (1)](/day21)
* Day 22：[箭頭函數 (Arrow Functions) 的 this 和你想的不一樣 (2)](/day22)
* Day 23：[ES6 物件實字威力加強版 (Enhanced Object Literals)](/day23)
* Day 24：[函數呼叫 (Function Invocation) 與立即函數 (Self-Invoking Functions)](/day24)
* Day 25：[不是多了塊魚 —— 立即函數的應用整理](/day25)
* Day 26：[程式界的哈姆雷特 —— Pass by value, or Pass by reference？](/day26)
* Day 27：[別管變數 Pass by Whatever，尋找容易理解的銀色子彈 (Silver Bullet)](/day27)
* Day 28：[閉包 (Closures)](/day28)
* Day 29：[閉包 (Closures) 進階打怪實戰](/day29)
* Day 30：[ES10 醞釀中 —— 擁抱 JS の 未來](/day30)


# Day 1：前言

近年 JavaScript 像個炙手可熱的大明星。

印象中，八、九年前的 JavaScript 並沒有那麼引人注目。當然，要寫網頁，HTML、CSS、JavaScript 仍是不可或缺的三劍客，但那還是個 Adobe Flash 稱霸網頁互動元件的風光年代，無論是網頁動畫、互動特效、桌面小遊戲，只要是多媒體互動的應用，Flash 與 ActionScript 幾乎是第一選擇。也有很多人選擇 Java Applet 作為網頁上的互動應用。相較之下，JavaScript 就像個跑龍套，只是傳統網頁實現簡單互動事件的工具，例如點點按鈕、跳出視窗，一點也不時尚。

![](https://fossbytes.com/wp-content/uploads/2017/07/rip-adobe-flash.jpg)\
(Source: [Dec 31, 2020: Adobe Flash Death Date Announced By Adobe](https://fossbytes.com/rip-adobe-flash-death-date-31-dec-2020/))

HTML5 的面世是個轉捩點，逐漸改變了一切。HTML5 增加了許多新語法元素，讓它可以做到許多傳統只能靠 Flash 或 Applet 才能實現的互動效果。尤其在[賈伯斯支持 HTML5 抵制 Adobe Flash ](https://www.apple.com/hotnews/thoughts-on-flash/)的論調公開後，越來越多開發者投入 HTML5，許多有趣的 HTML5 小遊戲如雨後春筍冒出來，讓人看到並驚嘆 HTML5 的無限潛力。而 HTML5 能實現各種豐富動態互動的背後功臣，就是 JavaScript。

到了近年，兩個重要因素更讓 JavaScript 的光芒推上高峰。

一個是 Node.js 的面世，讓 JavaScript 得以將「魔爪」伸入後端，一個語言前後端通吃，多麼有吸引力的優勢啊！讓眾多開發者趨之若鶩，紛紛投入 JavaScript 陣營。另一個是前端技術的爆炸性發展，堪稱是一日千里，眾家前端框架百花齊放，身為前端最重要的程式語言，JavaScript 自然前景一片看俏，社群蓬勃發展，每年都有新特性釋出，各種轉譯語言也百家爭鳴。

無論你使用哪一派前端框架、或是使用哪一個轉譯語言，對原生 JavaScript (ECMAScript) 都應該有一定的了解。

![](https://devhumor.com/content/uploads/images/October2018/baba-ji.png)\
(Source: [Where is JS Man?](https://devhumor.com/tags/javascript))

「JavaScript 很簡單」是許多人對它的印象。

確實，和 Java、C# 等嚴謹性高、規範多的語言比起來，JavaScript 是一個入門門檻相對容易許多的程式語言，可以歸功於它的許多特性：語法寬容、弱型別加動態型別、型別判斷籠統、非強制物件導向等等。但這些特性同時也使這個語言讓人又愛又恨——愛它容易起步上手，恨它藏著各種魔鬼的細節。JavaScript 的容易入門，常讓人輕忽了它的特性和陷阱，在開發過程大吃苦頭。

網路上很多人都看過這張嘲諷滿點的圖片「Thanks Brendan for giving us the Javascript」。我第一次看到的心得是：這是什麼神經病語言。在 Java 或 C# 開發者眼中，應該很容易有類似感想。(看 Brendan 那燦爛的笑容，笑得你心裡發寒啊\~)

![](https://i.redd.it/rz3o1yibnc511.png)\
(Source: [Thanks Brendan for giving us the Javascript : ProgrammerHumor - Reddit](https://www.reddit.com/r/ProgrammerHumor/comments/8srix1/thanks_brendan_for_giving_us_the_javascript/))

這個系列的目標是以清晰易懂的方式，探討原生 JavaScript 一些容易讓人混淆、造成編程失誤、或不那麼確切熟悉的特性，或是將一些觀念進行整理。預計也會探討部分 ES6、ES7、ES8 的新特性。

期待這個過程能像 2019 iT 邦幫忙鐵人賽的形象——「勇者鬥惡龍」，斬破 JavaScript 惡魔般的迷霧！

## References

* [Thoughts on Flash - Apple](https://www.apple.com/hotnews/thoughts-on-flash/)
* [Steve Jobs "Thoughts on Flash" 全文翻譯 | T客邦](https://www.techbang.com/posts/2405-steve-jobs-thoughts-on-flash-full-translation)
* [為什麼 Jobs 永遠不會讓 Flash 上 iPhone？ | MR JAMIE](https://mrjamie.cc/2010/04/30/why-jobs-will-never-allow-flash-on-iphone/)


# Day 2：資料型態的夢魘——動態型別加弱型別(1)

「JavaScript 是動態型別」，很多人會說知道。

「JavaScript 是弱型別」，也有很多人會說知道。

「JavaScript 是動態型別加弱型別」，可能很多人就不那麼肯定了。

許多人把這兩件事混淆，其實這是兩個不同的觀念。那麼，究竟**動態型別**和**弱型別**有什麼差別？

**動態型別加上過於寬鬆的弱型別，是我認為 JavaScript 最叫人頭痛的萬惡根源**，因此選擇以這部分做切入。

本篇文章先來看何謂靜態型別與動態型別。

## 靜態型別 vs. 動態型別

### 靜態型別的例子

我們先從比較容易理解的靜態型別來看。以 Java 為例：

```java
int x;
```

這是一個很簡單的變數宣告，沒有給初始值。請問 `x` 這個變數裡未來會放什麼類型的值？

答案是整數。

為什麼我能這麼篤定？因為在宣告 `x` 時，使用了 `int` 這個關鍵字，明確宣告了變數 `x` 是個整數變數。

如果我在使用過程，企圖在 `x` 內放入非整數的值，例如：

```java
int x;
x = "Hello";
```

編譯時就會得到以下錯誤：

```
HelloWorld.java:5: error: incompatible types: String cannot be converted to int
```

這就是典型的靜態型別。這類語言在型別的管理上十分嚴謹，在語法撰寫時就會要求對變數型別有明確定義，有時讓人覺得囉嗦；但相對的，一旦有變數誤用或資料型態上的 Bug，在編譯時期就能發現，降低執行時期的風險。

編譯式語言多半是靜態語言，Java 和 C# 是其中的代表。

### 動態型別的例子

用另外一個語言 Lua 來舉例：

```
local x
x = 123
print(x)
x = "Hello"
print(x)
```

可以發現在用 `local` 宣告變數 `x` 時，沒有事先明確指定 `x` 的型別是什麼，能放入任意類型的資料；甚至中間還一度改變指派值的資料型態，一開始放的是整數 `123`，後來改放字串 `"Hello"`，程式依然可以運作。

這就是一個動態語言的例子，這類語言在程式執行過程才會進行資料型態的檢查或確認，因此直譯式語言都是動態語言。如 Python、PHP、Ruby，還有我們的主角 JavaScript，都屬於此類語言。

這類語言在程式編寫時，不用花太多心思在宣告型別的語法上，簡潔而靈活，可以在過程依需求任意改變型別，做到十分靈活的變數處理；但相對缺乏對當前變數的型別限制，當執行到某一行程式時，無法絕對肯定 `x` 變數現在放了什麼類型的值，容易造成非預期的執行可能性，導致非預期的執行結果。也就是容易埋下 Bug！

### 宣告時沒有指定型別，就是動態型別？

從上面兩個例子可以明顯看出動態語言和靜態語言的差別。可能會覺得：「很好辨認嘛，看宣告變數時有沒有指定型別，就知道是不是動態語言」。

這個理解是不精確的。

因為有的靜態語言在宣告時，也不需要指定型別，是透過**隱性推導**的方式來確認型別，例如 Ocaml、Haskell。

像傳統 C# 是典型的靜態語言，但在 C# 4.0 添加了新特性，允許宣告時不指定型別，但不改變 C# 骨子裡是靜態語言的事實。

例如傳統 C# 的撰寫方式如下：

```
int n = 123;
Console.WriteLine(n);
```

C# 4.0 後允許用以下方式撰寫：

```
var n = 456;
Console.WriteLine(n);
```

看起來很像動態語言的寫法。但如果企圖在過程放入不同型別的值，在編譯時一樣會被檢查出來，也就是說 C# 骨子還是靜態語言，不允許動態語言的使用方式，比如使用過程任意改變型別。

例如以下例子，將發生 `Cannot implicity convert type 'string' to 'int'` 的編譯錯誤(Compilation Error)：

```
var n = 456;
Console.WriteLine(n);
n = "Hello";
```

可能會有疑問：既然沒有指定型別，編譯器怎麼知道 `n` 的型別應該是什麼？

以 C# 4.0 來說，`n` 的型別就是**初始值的型別**，所以宣告時必須被初始化，否則會有 `Implicity-typed variables must be initialized` 的編譯錯誤，如以下例子：

```
var n;
n = 123;
Console.WriteLine(n);
```

這種不在語法上指定，而靠隱性推導的方式，稱為**隱性型別(implicity typed)的靜態語言**。

C# 4.0 添加了這個特性，算是吸收了動態語言在宣告語法上簡便的優點，同時仍保有靜態語言的穩定性。

### 總結靜態語言和動態語言的定義和差別

**靜態語言(Statically Typed Languages)：**

* 型別檢查(Type Checking)發生在**編譯時期(Compile Time)**。
* 程式撰寫時必須使用明確的型別宣告。
* 型別一旦宣告後，在執行時期時無法任意更換型別，否則會發生錯誤。

**動態語言(Dynamically Typed Languages)：**

* 型別檢查(Type Checking)發生在**執行時期(Runtime)**。
* 程式撰寫時不用明確的型別宣告。
* 執行時，變數能任意更換型別。

**而靜態語言又分為：**

* 顯性型別(explicitly typed)：型別是語法宣告的一部份，從語法就能得知。
* 隱性型別(implicity typed)：型別是透過編譯過程的推導得知。

## Reference

* [靜態語言 vs. 動態語言的比較](https://blog.sina.com.tw/dotnet/article.php?entryid=614009)
* [動態語言與靜態？直譯與編譯？強型別與弱型別？](https://millenniummeetonce.blogspot.com/2018/04/blog-post_5.html)
* [弱类型、强类型、动态类型、静态类型语言的区别是什么？](https://www.zhihu.com/question/19918532)


# Day 3：資料型態的夢魘——動態型別加弱型別(2)

上一篇介紹了動態型別和靜態型別的差別，這一篇來看到弱型別和強型別。

## 強型別 vs. 弱型別

### 強型別的例子

強型別的例子，我們一樣拿型別界的乖寶寶 —— Java 為例，企圖在數字運算過程混進一個字串：

```java
int x = 123 + "456";
System.out.println(x);
```

不出所料，得到一個編譯錯誤：

```
HelloWorld.java:4: error: incompatible types: String cannot be converted to int
```

很明確的告訴你，在 Java 不能用**隱喻**的方式，企圖直接將字串轉成整數，如果你想實現的是將「數字 `123` 」和「字串 `"456"` 轉成的數字」相加，你必須用 Java 能接受的方式明確表明，比如：

```java
int x = 123 + Integer.parseInt("456");
System.out.println(x);
```

執行結果：

```
579
```

這就是強型別的語言，**偏向不容忍隱性的型別轉換**。

同樣的寫法，換成弱型別會發生什麼事？

### 弱型別的例子

以 PHP 為例：

```php
$x = 123 + "456";
echo $x;
```

執行結果：

```
579
```

不僅成功執行，沒有錯誤訊息，還成功印出字串 `"456"` 轉成數字後的相加結果。

這就是弱型別的語言，**這類語言偏向容忍隱性的型別轉換**。雖然沒有在程式碼裡指明我想將字串 `"456"` 轉成數字，PHP 直譯器發現我在做算術運算，而且 `"456"` 可以被轉成數字，就「貼心」地自動幫忙轉成數字，然後吐出相加的結果。

可能會說：這樣不是很好嗎？不用什麼事都要在語法上囉哩吧嗦指定清楚，程式自動就能「體察上意」。

在許多時候確實很方便省事，但有時真的不小心打錯程式，電腦不會知道你是故意還是不小心，一樣會自作聰明去「猜」你想做什麼，自動幫你進行你不想要的轉型。

例如下面這個例子：

```php
$n1 = 123;
$n2 = 456;
$s1 = "Hello";
$x = $n1 + $s1;
echo $x;
```

執行結果：

```
123
PHP Warning:  A non-numeric value encountered in /home/cg/root/6938116/main.php on line 4
```

我想做的其實是 `$x = $n1 + $n2`，手誤為 `$x = $n1 + $s1`，PHP 直譯器發現我在做算術運算，於是擅自將 `"Hello"` 硬轉型成數字，得到 `0`，繼續產生執行結果 `123`。而不是盡早提示錯誤、終止程式，必須等開發者自己發覺。

確實，`"Hello"` 根本不適合轉成數字，PHP 直譯器覺得怪怪的，幫忙顯示一個 PHP Warning 來提醒開發者。但 Warning 不等於 Error，對電腦來說，這段程式碼仍是成功運作，會繼續往下面的程式碼執行。但實際上我們得到了非預期的結果，後續執行的程式碼連帶可能也都是錯誤的結果。

> PHP 環境設定需要開啟 Warning 才會印出警告，開發環境通常會開啟以便 debug，生產環境通常會關閉。

弱型別最常導致 Bug 的情境，除了混用不同型別去數學運算或字串串接，另一個就是**允許不同型別之間的比對**。

以 PHP 為例，要比對兩個值，提供了兩種運算子：

* `==`：寬鬆比對，只比對兩值的**內容**。
* `===`：嚴謹比對，比對兩值的**型別**和**內容**。

```php
echo ("111" == 111) ? "Yes" : "No";     // "Yes"
echo ("111" === 111) ? "Yes" : "No";    // "No"
```

### 總結強型別和弱型別的定義和差別

* 強型別(strongly typed)：偏向不容許隱性型別轉換，型別檢查上較為嚴格。
* 弱型別(weakly typed)：偏向容許隱性型別轉換，型別檢查上較為寬鬆。

簡單來說，就是**編譯器或直譯器對型別檢查的寬容程度**。

或更淺白地形容：**允許編譯器或直譯器自作主張的程度**。

強型別語言偏向說一是一、說二是二，你沒有在程式語法上明確指示就是沒這件事，發現不是正常寫法，直接停下來告訴你發生錯誤。

弱型別語言就不同了，發現不是正常寫法，會試圖去做一些自動轉型，讓這段程式繼續運作下去。

至於孰優孰劣？其實沒有絕對標準。

普遍來說，強型別當然比較嚴謹，很多潛藏的 Bug 可以在編譯時期甚至撰寫時期就發現；但也代表開發過程可能需要花許多時間去雕琢語法來符合強型別的語法規範限制。

而弱型別風險相對較高，但撰寫時不會有那麼多限制。

### 強或弱不是非 1 則 0

值得一提的是，**強型別和弱型別不是 1 或 0 二元論，而有「程度」的差別**。

雖然 PHP 是弱型別，偏向容許隱性型別轉換，但還是有個容忍的限度。

例如企圖將數字和陣列做算術運算，就不再是無傷大雅的 Warning，而會得到 Error：

```php
$x = 123 + array("Apple", "Banana");
echo $x;
```

執行結果：

```
PHP Fatal error:  Unsupported operand types in /home/cg/root/6938116/main.php on line 3
```

### 靜態語言一定是強型別？動態語言一定是弱型別？

答案是：NO。

「靜態型別/動態型別」和「弱型別/強型別」代表不同的意義：

* 靜態型別/動態型別：變數和型別的綁定方法。
* 弱型別/強型別：語言型別系統(Type System)對型別檢查的嚴格程度，也就是**型別安全的程度**。

舉例來說，Python 是一個動態語言，以下例子企圖對 Python 的數字和字串進行相加：

```python
x = 123 + "Hello Python"
print x + "\n"
```

會得到以下錯誤：

```
TypeError: unsupported operand type(s) for +: 'int' and 'str'
```

因為 Python 雖然是個動態語言，但在型別判斷的嚴格程度上，Python 是一個強型別。

雖然會有些趨勢，例如常看到的靜態語言大部分是強型別。但本質來說，「靜態型別/動態型別」和「弱型別/強型別」沒有絕對關係。

### 常見語言的型別特性

以下是幾種常見程式語言的型別特性：

| 靜態語言 / 動態語言 | 強型別 / 弱型別 | 程式語言                  |
| ----------- | --------- | --------------------- |
| 靜態          | 強         | Java, C#              |
| 靜態          | 弱         | C/C++                 |
| 動態          | 強         | Python, Ruby          |
| 動態          | 弱         | Perl, PHP, JavaScript |

靜態語言又分顯性型別和隱性型別：

* 靜態顯性型別：Java, C
* 靜態隱性型別：Ocaml, Haskell

下面這張象限圖可以非常清楚看到不同語言各自屬於哪個象限：

![](https://2.bp.blogspot.com/-z6nC9JYj8vY/WsWy73hIO8I/AAAAAAAAT4I/3DhB8aOI2JAQTJumnGOEPq2HtgF_R4s0wCLcBGAs/s400/b0aeb7ffd1667b9162e5329154d43777_hd.jpg)\
(Source: [動態語言與靜態？直譯與編譯？強型別與弱型別？](https://millenniummeetonce.blogspot.com/2018/04/blog-post_5.html))

### 同場加映：C 語言是弱型別？

這個系列的主題是 JavaScript，本節算是一個對強型別/弱型別的額外探討。

在型別特性的分類上，C 語言被歸類在弱型別，可能比較叫人意外，因為 C 語言的變數無論在宣告或是使用上都非常囉嗦嚴格。

有些文章提到的理由是：因為 C 的 `int` 可以變成 `double`。

這個理由漏洞很大，因為 Java 的 `int` 也容許被隱性轉成 `double`：

```java
double x = 123;
System.out.println(x); // 123.0
```

那為何 Java 被公認為強型別，C 語言卻被歸類在弱型別？

以下是我目前的簡易理解。

這要來看一下強型別更精確但文謅謅的定義：

> 強型別(strongly typed)：一種語言的所有程式都是 well behaved —— 也就是不可能出現 forbidden behaviors。

C 語言有一個特殊武器 — 指標(pointer)，可以對變數記憶體做更細緻的操作，是 C 語言靈活強大的武器，也是各種令人頭痛的記憶體錯位 Bug 溫床。

使用指標有可能發生例如 `int` 的記憶體 size 卻允許在語法上放入 `double` 甚至更大的值，導致記憶體溢位，屬於 **forbidden behaviors**，因而 C 語言被歸類為弱型別：

```c
int value = 12345;
int* ptr = &value;
*ptr = 999999999999999999999999999; // overflow
printf("%d\n", *ptr);
```

「C 的 `int` 可以變成 `double`」這句話也許沒錯，只是有點語病，想表達的意思應該是：「C 的 `int` size 的變數，語法上容許被指派 `double` size 值的可能性」。

## References

* [動態語言與靜態？直譯與編譯？強型別與弱型別？](https://millenniummeetonce.blogspot.com/2018/04/blog-post_5.html)
* [弱类型、强类型、动态类型、静态类型语言的区别是什么？](https://www.zhihu.com/question/19918532)


# Day 4：動態型別加弱型別不是罪——怎麼 JavaScript 一摔就變成個印度阿三？

前兩篇文章介紹了什麼是動態型別、靜態型別、強型別、弱型別，也知道了幾個常見語言分屬哪種類別。

懶人包複習一下：

> * 靜態語言/動態語言：變數和型別的綁定方法。
> * 弱型別/強型別：語言型別系統(Type System)對型別檢查的嚴格程度，也就是型別安全的程度。

其中**動態型別加弱型別**是最不安全的組合，也是我認為 JavaScript 撰寫上鬆散不安全的源頭之一。

但怪了，動態型別加弱型別的語言又不是只有 JavaScript，同樣使用者眾多的 PHP 也是屬於動態型別加弱型別，為何 PHP 不像 JavaScript 這麼讓人詬病？

套句港片「賭俠」裡星爺的台詞：我摔你也摔，怎麼你一摔就變成個印度阿三啦？

![](https://i.imgur.com/TBWgV4I.png)\
(Source: [網路](https://gss0.baidu.com/9vo3dSag_xI4khGko9WTAnF6hhy/zhidao/pic/item/b8389b504fc2d562a46680a3e71190ef76c66c15.jpg))

**動態型別加弱型別不是罪，動態型別加超級弱型別就是一場夢魘，更別說攪和了 JavaScript 自身型別系統(Type System)的混亂**。

很多問題是相對性的，需要經過比較才會凸顯出來，因此前兩篇文章刻意都以其他語言做舉例，本篇文章將把目光拉回 JavaScript，用實例來檢視 JavaScript 在型別處理上的各種特性，透過和其他語言的比較，將更能體會 JavaScript 在型別安全上的脆弱。

究竟～是動態型別加弱型別的原罪使然，還是型別系統的命運糾葛，讓我們繼續看下去～

![](https://i.imgur.com/hbWSAyj.png)\
(Source: [網路](https://v1c.im.tv/beta/2014/04/ART1404281738-86793-Icon.jpg))

## 動態型別

前面介紹過動態型別和靜態型別的差別，現在我們可以很清楚判斷 JavaScript 屬於動態語言：

* 宣告變數時不需指定型別。
* 過程可以任意更換變數的型別。

```javascript
var x = 123;
console.log(x);
x = 'Hello';
console.log(x);
```

執行結果：

```
123
Hello
```

## 弱型別

Day3 文章介紹到，弱型別常導致 Bug 的一個情境就是**允許不同型別間的比對**。

這部分 JavaScript 和 PHP 一樣，提供了兩種嚴格程度不同的比較運算子：`==`、`===`

```javascript
console.log( "111" == 111 ? "Yes" : "No" );     // "Yes"
console.log( "111" === 111 ? "Yes" : "No" );    // "No"
```

另一個情境是如果在數字運算過程混進一個字串，會發生什麼事？

強型別的 Java 會發生編譯錯誤；弱型別的 PHP 會企圖將字串轉成數字，以完成算術運算的任務。

那 JavaScript 呢？

```javascript
var x = 123 + "456";
console.log(x);
```

執行結果：

```
123456
```

同樣是弱型別的 JavaScript，執行時一樣不會發生錯誤，會自動做一些轉型動作以讓程式繼續運行下去。

不同的是，JavaScript 不會去將字串轉成數字，反而是將前面的數字結果轉成字串，再和後面的字串作串接，所以得到 `"123456"` 的結果。

幾乎一模一樣的語法情境，為什麼 JavaScript 的處理邏輯和 PHP 截然不同？

## 數學運算？字串串接？

用另一個例子來看，差異會更明顯。

如果是 PHP，以下寫法會得到單純的數字運算結果，也就是一個整數型態的 `999`：

```php
<?php
$x = 111 + 222 + "333" + 111 + 222;
echo $x;
?>
```

執行結果：

```
999
```

如果是 JavaScript 呢？

```javascript
var x = 111 + 222 + "333" + 111 + 222;
console.log(x);
```

執行結果：

```
333333111222
```

![](https://i.imgur.com/Xx9HKan.png)\
(Source: [網路](https://vignette.wikia.nocookie.net/evchk/images/e/ec/2471912.jpg/revision/latest?cb=20171012125530))

是一個讓人黑人問號的答案呢。

程式不是魔術師，其實有跡可循。前面的 `111 + 222` 被視為算術運算做相加，得到數字 `333` ；繼續和中間的字串 `"333"` 相加，被視為字串的串連，得到 `"333333"`；而最後的 `111` 和 `222` 雖然是數字型態，都被以字串相接的方式處理，以至於得到 `"333333111222"` 這麼滑稽的答案。

為什麼一樣都是整數型態的 `111`、`222`，JavaScript 連自己前後處理的方式都不一樣？

在 JavaScript 裡，如果我們想對兩個變數的值做**數學相加**，語法表現方式如下：

```javascript
var v1 = 111;
var v2 = 222;
var x = v1 + v2;    // 333
```

如果想對兩個變數的值做**字串串接**，語法表現方式如下：

```javascript
var v1 = "111";
var v2 = "222";
var x = v1 + v2;    // "111222"
```

發現問題了嗎？無論是**數學相加**，還是**字串串接**，用的都是同一個符號 `+` 來表達，事實上他們是不同的運算子：

* 用在數學相加時：Arithmetic Operators (算數運算子)
* 用在串連字串時：Concatenation Operators (串接運算子)

那 JavaScript 怎麼決定它要把眼前的 `+` 視為**算數運算子**還是**串接運算子**？

JavaScript 的做法是：如果 `+` 左右兩邊的運算元有任何一個型別是字串，就會被當作**串接運算子**，否則就是**算數運算子**。

不幸的，由於 JavaScript 是**動態型別**，單從 `var x = v1 + v2;` 這行程式本身，我們無法判斷 JavaScript 會將 `+` 當成算數運算子還是串接運算子，因為變數 `v1`、`v2` 有可能是任何型別。

這就是為什麼 JavaScript 常常容易出現一些意想不到的相加結果。

可能會有疑問：為什麼 PHP 沒這個問題？

很簡單，因為 PHP 的算數運算子和串接運算子是不同符號，PHP 的串接運算子是 `.`：

```php
<?php
$v1 = 111;
$v2 = 222;
$x = $v1 + $v2; // 333
$x = $v1 . $v2; // "111222"
?>
```

另一個動態型別加弱型別的語言 Perl 也有同樣的設計，讓數學運算和字串串接可以被清楚區隔。

## 超級弱型別

Day3 的文章提到一點：**強型別和弱型別不是 1 或 0 二元論，而有「程度」的差別**。

例如 PHP 是弱型別，但還是有個容忍的限度，例如將數字和陣列做算術運算就會發生 Error：

```php
$x = 123 + array("Apple", "Banana");
echo $x;
```

```
PHP Fatal error:  Unsupported operand types in /home/cg/root/6938116/main.php on line 3
```

JavaScript 要命在於它是**超級弱型別**！

幾乎什麼都能相加、什麼都能比較、什麼都不奇怪，一堆毫無邏輯的寫法在 JavaScript 通通都可以執行！

```javascript
console.log( [] + [] );                     // ""
console.log( [] + {} );                     // "[object Object]"
console.log( [] == {} );                    // false
console.log( [] == false );                 // true
console.log( [] == 0 );                     // true
console.log( ['Apple', 'Banana'] == 2 );    // false
console.log( ['Apple', 'Banana'] == true ); // false
console.log( 123 + ['Apple', 'Banana'] );   // "123Apple,Banana"
console.log( true + true + true );          // 3
console.log( false + false + false );       // 0
```

![](https://i.imgur.com/BmP9xAt.png)\
(Source: [臭跩貓愛嗆人7-白爛貓超級胖](https://store.line.me/stickershop/product/1382681/zh-Hant?from=sticker))

**動態型別加弱型別**不是問題，**動態型別加超級弱型別**就變成一場夢魘。

## 令人混淆的型別系統

JavaScript 自身型別系統的設計也讓人感覺不夠嚴謹，或是令人困惑。

### null 都不 null 了

最具代表性的型別問題應該就是「`null` 是個 object」的弔詭現象。

`null` 的概念在各種程式語言很普遍，代表「nothing」，也就是這個變數裡面沒有值。

但在 JavaScript 裡如果用 `typeof` 去取得其型別，卻會回傳 `"object"` 的結果。

[W3Schools](https://www.w3schools.com/js/js_datatypes.asp) 也稱呼這是一個 JavaScript 的 Bug：

> You can consider it a bug in JavaScript that typeof null is an object. It should be null.

### 獨特的 undefined 型別

JavaScript 除了常見的 `null` 型別，還有另一個特別的型別：`undefined`。

`undefined` 在概念上和使用上都非常類似 `null`，但在 JavaScript 的型別系統中他們又確確實實屬於不同的型別。

事實上，在 JavaScript 程式，比起 `null` 型別，`undefined` 更接近其他語言對 `NULL` 的習慣。

(例如去印一個沒有初始化的變數，會得到 `undefined` 而不是 `null`)

```javascript
var x;
console.log(x); // "undefined"
```

相形之下，`null` 就顯得很雞肋(而且是有 Bug 的雞肋...)，徒增混淆。

## JavaScript 的型別夢魘真的沒救了嗎？

當然不。

JavaScript 終究是動態網頁不可缺少的角色，為了降低開發 JavaScript 程式的風險、提升程式穩定性，許多轉譯語言被發明出來，例如 [TypeScript](https://www.typescriptlang.org/) 是近年竄起的代表。

這些轉譯語言試圖在語法上引入一些特性，例如強型別、物件導向，讓 JavaScript 的程式撰寫更容易、更安全、更易於管控。

短期來說，學習這些轉譯語言需要增加額外的時間成本；但長期而言，當專案規模越來越大，轉譯語言有助於提升 JavaScript 程式開發和維護上的穩定性。

各家轉譯語言各有不同特性，是否應該導入轉譯語言、導入哪一種，沒有絕對答案，視乎各團隊的技能狀況和專案性質，權衡其中的成本效益和風險。

## References:

* [W3Schools - JavaScript Data Types](https://www.w3schools.com/js/js_datatypes.asp)
* [TypeScript 官方網站](https://www.typescriptlang.org/)


# Day 5：湯姆克魯斯與唐家霸王槍——變數的作用域(Scope) (1)

一個變數什麼時候開始發揮作用？這個問題的答案很單純——當一個變數被宣告。

那一個變數什麼時候失效？這個問題的答案就複雜多了。

![](https://ithelp.ithome.com.tw/upload/images/20171223/20107429aYpMCDIU6H.jpg)\
(Source: [網路](https://ithelp.ithome.com.tw/upload/images/20171223/20107429aYpMCDIU6H.jpg))

俗話說：「出來混，遲早要還」，變數們在程式裡走跳，不會每個都長命百歲。

有的變數活得久，有的只作用了幾行程式碼。

有的變數只在自己的地盤——函數裡被認得；有的變數就像好萊塢大明星，在程式各個角落都有影響力。

一切，取決於這個**變數誕生的地方與宣告的方式**。

**一個變數的地盤有多大、能生效的範圍有多廣，就稱為這個變數的作用域(Scope)**。

本篇文章就來盤點 JavaScript 的變數作用域有哪些。

## 變數的作用域就像明星的地盤

**作用域**這術語聽起來很高深嚇人，但要理解它並不像它的名稱那麼困難。

可以把它想像成**地盤知名度**的概念，**變數就是明星**。

不同等級的明星會隨著他們發跡地點不同，在不同地域有不同的影響力，在某些地方知名度太低，觀眾就不會理他。

兩岸三地紅透半邊天的華人演員，在歐美可能默默無聞或跑跑龍套。

每次看到演技精湛的華人演員在好萊塢只能演花瓶或雞肋，就覺得非常感嘆，目前仍是個西方娛樂文化強勢當道的世界。

例如我極欣賞的一部電影「十月圍城」，裡面以不慍不火的沉穩演技而貫穿全片的大腕演員王學圻，在「鋼鐵人 3」裡只能飾演一個沒什麼存在感的醫生角色。(我甚至想不太起來他的橋段……) ![](https://i.imgur.com/5w180ba.png)\
(Source: [網路](https://vpic.video.qq.com/4269112134/y00123ggapv_ori_5.jpg))

而好萊塢巨星往往就有跨界的影響力。

例如今年轟動一時的新聞：好萊塢巨星湯姆克魯斯參加韓國知名綜藝節目 Running Man，雖然不同國情、文化、地域，身為國際巨星的阿湯哥依然仍發揮他的跨界影響力，為 Running Man 創下歷史高收視。 ![](https://i.imgur.com/iFJjqNj.png)\
(Source: [Youtube](https://www.youtube.com/watch?v=URN9TCmoJDQ))

知名度對明星來說既現實又殘酷，就像作用域之於變數。

同樣是宣告一個變數，有的變數只有在他宣告的地盤才有作用；有的變數即使不是宣告在這個區塊，依然能跨界發揮影響力。

**變數作用域的範圍，取決於這個變數宣告的地方與方式**。

## JavaScript 的作用域有 3 個等級

在 JavaScript 裡，有 3 種等級的作用域： 1. 香港喜劇天王星爺——Function Level Scope 2. 國際巨星阿湯哥——Global Level Scope 3. 住在隔壁號稱歌神的里長阿伯——Block Level Scope (ES6)

下面我們一一來看到。

## Function Level Scope

先從最容易理解的 Function Scope 開始。

![](https://i.imgur.com/kLTbvWS.png)\
(Source: [網路](https://pic.pimg.tw/taker/1191231257_n.jpg))

只要有心，人人都可以是工程師！

喔不對，是食神。

周星馳的無厘頭搞笑文化早就深植我們的生活中(百萬鄉民站出來！)，看過星爺電影的幾乎人人都能朗朗上口幾句經典台詞。

但是如果問一個老外，比如美國人、英國人、法國人、印度人，他們可能毫無感覺。

這就是 **Function Level Scope**，在一定的地域內有著影響力，一旦超過這個地域，可能就被當無名小卒。

這種變數我們常稱為**區域變數(Local Variables)**，也是一般寫程式運用最頻繁的 Scope Level。

基本的 Function Scope 很單純，也最好判斷，地盤就是以 Function 為限，在 Function 之外就不認得。

下面是一個 Function Scope 例子：

* 宣告位置：function 內。
* 有效範圍：該 function 之內。
* 說明：
  * 在 Function 內，從宣告開始到最後都有效。
  * 在 Function 外就變成未定義。

```javascript
function myFunc(){
    var n1 = "OneJar";
    console.log("myFunc():  n1=", n1);
}

myFunc();
console.log("Global: typeof n1=", typeof n1); // 這裡 n1 只能印 type 不能印值，否則會拋 `ReferenceError: n1 is not defined`
```

執行結果：

```
myFunc(): n1= OneJar
Global: typeof n1= undefined
```

> *作者閒聊：其實這裡本來想從幾個華語電影公認演技派的巨星作為 Function Scope 舉例，例如劉德華、梁朝偉、梁家輝，我覺得更貼切。*
>
> *尤其是華仔，稱為華人界有巨大廣泛影響力的國民巨星也不為過，但很遺憾至今仍未跨足好萊塢，西方娛樂市場的文化高牆無法靠演技就能收服。就像 Function Scope 裡再重要的變數，也難以將效用突破環境的作用域限制。*
>
> *這算是身為一個工程師，同時是華語電影愛好者的感嘆。*
>
> *不過最後猶豫很久，還是決定偷渡我最鍾愛的星爺。*

## Global Level Scope

在每個執行 JavaScript 程式的環境，會有一個**全域物件 (Global Object)**：

* 在 HTML 裡，全域物件是 `window` object。
* 在 Node.js 裡，全域物件是 `global` object。

存放在全域物件裡的變數，**無論在哪裡宣告，效力都能遍及整個程式**，我們稱為**全域變數 (Global Variables)**。

就像好萊塢明星，即使只是歐美發跡，無論中國、日本、韓國、印度，在全球範圍內都有他們的影響力。

下面是一個 Global Scope 例子：

* 宣告位置：主程式內，任何 Function 之外。
* 有效範圍：整個程式，包含所有 Function 內。
* 說明：
  * 無論 Function 內外都能使用。
  * 呼叫方式可以是直接呼叫變數名稱，或透過全域物件 `window` 去呼叫。

```javascript
function myFunc(){
    console.log("myFunc(): n1=", n1);
    console.log("myFunc(): this.n1=", this.n1);
    console.log("myFunc(): window.n1=", window.n1);
}

var n1 = "OneJar";
myFunc();
console.log("Global: n1=", n1);
```

執行結果：

```
myFunc(): n1= OneJar
myFunc(): this.n1= OneJar
myFunc(): window.n1= OneJar
Global: n1= OneJar
```

## Local 內的變數有可能自動轉成 Global 變數

在 JavaScript 裡有一種狀況**會自動產生全域變數**，那就是**賦值給未宣告的變數**。

```javascript
function myFunc(){
    n1 = "OneJar";  // 自動變成一個 Global 變數
    console.log("myFunc(): n1=", n1);
    console.log("myFunc(): this.n1=", this.n1);
    console.log("myFunc(): window.n1=", window.n1);
}

myFunc();
console.log("Global: n1=", n1);
```

執行結果：

```
myFunc(): n1= OneJar
myFunc(): this.n1= OneJar
myFunc(): window.n1= OneJar
Global: n1= OneJar
```

雖然不是在函數外，而是函數內動作，但 `n1` 沒有被宣告過就進行賦值，在 JavaScript 行為會**自動**把 `n1` 變成一個全域變數。

一般來說應該**避免這種寫法**。

全域變數的影響力遍及整個程式，不應該隨便產生不需要的全域變數，造成額外的程式風險，以及變數控管上的困擾。

如果真的想要產生一個全域變數，應該在適當的地方進行明確的宣告，例如在函數外，或透過 `window` 物件。而不是在某個 Local 裡用這種隱晦語意的方式產生。

> 在 "Strict Mode" 下，不會自動產生 Global 變數。關於 "Strict Mode" 預計之後另外介紹。

## References

* [W3Schools - JavaScript Scope](https://www.w3schools.com/js/js_scope.asp)


# Day 6：湯姆克魯斯與唐家霸王槍——變數的作用域(Scope) (2)

## 星爺強碰阿湯哥誰會贏？Global Scope vs. Function Scope

![](https://i.imgur.com/KkQVLtA.png)\
(Source: [網路1](https://puui.qpic.cn/qqvideo_ori/0/k0505psezun_496_280/0) / [網路2](https://globedia.com/imagenes/noticias/2015/9/4/mision-imposible-tom-cruise-corre-horas_1_2287136.jpg))

俗話說天高皇帝遠，十里外的瀑布不如眼前的一杯水。

全球來說，在杜拜第一高樓跳來跳去的阿湯哥毫無疑問有較廣泛的知名度。但對我們而言，星爺在我們的生活中有更深的影響力。

Global 變數和 Local 變數也是如此。

也許**對整個程式來說，Local 變數不及 Global 變數效用來得廣**，但**在函數的 Local 範圍內，Local 變數的存在感比較高**。

所以如果 Global 和 Function 內宣告了同樣名稱的變數，在 Function 內會是 Local 變數生效，Function 外依然是 Global 稱王。

如下面這個例子：

```javascript
function myFunc(){
    var n1 = "Stephen Chow";
    console.log("myFunc(): n1=", n1);
    console.log("myFunc(): this.n1=", this.n1);
    console.log("myFunc(): window.n1=", window.n1);
}

var n1 = "Tom Cruise";
myFunc();
console.log("Global: n1=", n1);
```

執行結果：

```
myFunc(): n1= Stephen Chow
myFunc(): this.n1= Tom Cruise
myFunc(): window.n1= Tom Cruise
Global: n1= Tom Cruise
```

要注意的是，如果在 Function 內透過如 `window.n1` 的方式，明確表明「**我要取的是 Global 變數的 `n1`，而不是 Local 的 `n1`**」，一樣會是 Global 變數發揮效用。

## Block Level Scope

除了 Global Level 和 Function Level，還有第三種等級：Block Level。

![](https://i.imgur.com/PPHpxvt.jpg)\
(Source: [網路](https://i.ytimg.com/vi/GbBdx9uwe4o/maxresdefault.jpg))

Block Level 就像住在你家隔壁號稱歌神的里長阿伯。

不要小看他們，也許不要說離開這個縣市，可能出了這一里就沒人認得他們，但在這一里的範圍內，他們就是婆婆媽媽們心中最強的情歌王子。

Block Level 的作用域範圍可能非常小，只是一個函數裡的某一段程式。

程式裡 Block 指的是一段用大括號 (`{` 和 `}`) 包起來的區塊。

我們用 Java 來舉例：

(為什麼用 Java 舉例而不是用 JavaScript，你馬上就會知道)

### Block Scope 的 Java 例子

以下是一段 Block 的例子：

```java
public static void main(String []args){
    {
        int x = 10;
        System.out.println(x);
    }
}
```

執行結果：

```
10
```

很正常印出 `x` 的值，好像沒什麼異狀。

我們改在 Block 外面去呼叫 `x` 變數：

```java
public static void main(String []args){
    {
        int x = 10;
    }
    System.out.println(x);
}
```

執行結果：

```
HelloWorld.java:7: error: cannot find symbol
        System.out.println(x);
                           ^
  symbol:   variable x
  location: class HelloWorld
1 error
```

Java 編譯器說他不認得 `x` 這個變數。

這就是 Block Scope 的效果。雖然在同一個 Function 內，只要一出 Block，變數就失去效用。

### JavaScript 的 `var` 在 Block Scope 使用的例子

輪到 JavaScript 上場了！

我們一樣在 Block 內宣告變數，預期出了 Block 就不認得 `n1` 變數：

```javascript
{
   var n1 = "OneJar";
}
console.log("Global: n1=", n1);
console.log("Global: this.n1=", this.n1);
console.log("Global: window.n1=", window.n1);
```

執行結果：

```
Global: n1= OneJar
Global: this.n1= OneJar
Global: window.n1= OneJar
```

![](https://i.imgur.com/Xx9HKan.png)\
(Source: [網路](https://vignette.wikia.nocookie.net/evchk/images/e/ec/2471912.jpg/revision/latest?cb=20171012125530))

怎麼還是印得出來？而且是個全域變數。

一定有什麼誤會，可能只有 Function 內的 Block 有效，我們改在 Function 內的小 Block 去宣告：

```javascript
function myFunc(){
    {
       var n1 = "OneJar";
    }
    console.log("myFunc(): n1=", n1);
    console.log("myFunc(): this.n1=", this.n1);
    console.log("myFunc(): window.n1=", window.n1);
}

myFunc();
```

執行結果：

```
myFunc(): n1= OneJar
myFunc(): this.n1= undefined
myFunc(): window.n1= undefined
```

![](https://i.imgur.com/FKU24ly.png)\
(Source: [網路](https://vignette.wikia.nocookie.net/evchk/images/e/ec/2471912.jpg/revision/latest?cb=20171012125530))

還是印得出來，雖然不再是 Global 變數，但看起來是 Function Level Scope，而不是上面所描述的 Block Scope。

到底怎麼回事？

## 對，傳統 JavaScript 只有 Global 和 Function 兩種等級的作用域

![](https://i.imgur.com/GLkuwLF.png)\
(Source: [Youtube](https://www.youtube.com/watch?v=o9US59aO71s))

剛剛一大段 Block Scope 都在講辛酸的嗎？

放心，都找了舞棍阿伯出來站台，怎麼可能是假的。

確實**在 ES5 以前，用 `var` 關鍵字去宣告的變數，只會有 Global Level 和 Function Level 兩種等級的作用域**。

**ES6 導入了新的變數宣告關鍵字：`let` 和 `const`，不僅提高變數控管的嚴謹性，也增加了 Block Scope 的用途**。

明天後面的文章我們就來介紹 ES6 的 `let` 和 `const`。

## References

* [W3Schools - JavaScript Scope](https://www.w3schools.com/js/js_scope.asp)


# Day 7：傳統 var 關鍵字的不足

長久以來 JavaScript 宣告變數所用的關鍵字 (Keyword) 都是 `var`。

大家已經非常習慣，甚至在 `let` 和 `const` 導入這麼久之後，仍時常看到 `var` 的芳蹤。(當然一部份原因跟 ES6 支援度的普及有關係)

但傳統 `var` 有幾個缺點，容易造成程式編寫上的不嚴謹，進而導致變數控管困擾。

ES6 導入了新的宣告關鍵字—— `let` 和 `const`，有助於改善原本 `var` 不嚴謹的缺點，進而增加新的特性，例如 Day5、Day6 文章提到的舞棍阿伯……不對，是 Block Scope。

在介紹 `let` 和 `const` 之前，我們要先了解傳統 `var` 有什麼缺點。

## 學程式很重要的一點是要思考「為什麼」

程式語言的變革非常快，不同語言間時常會互抄，我是說，互相學習。

各家語言的主事者不斷推出新版本，例如常聽到的 C# 4.0、Java 9、PHP 7，就是為了不斷導入新特性或修正原先的設計缺失，讓自家語言的功能更加豐富和周全。

而這些新特性的導入往往都有其歷史原因或背後考量，不會是突然間靈光一閃，上帝啟示。

![](https://i.imgur.com/TV7vGiN.png)\
(Source: [網路](https://images2017.cnblogs.com/blog/1001567/201708/1001567-20170820105857271-267973052.png))

如果只知其然而不知其所以然，只是去背用法，但不知道為什麼需要這樣用，不僅無法融會貫通，在運用上也不容易到位。

因此在介紹 `let` 和 `const` 之前，我們要先了解傳統 `var` 有什麼缺點，才能理解為什麼需要導入 `let` 和 `const`，對程式又能有什麼幫助。

## 傳統 `var` 的變數宣告方法

傳統 `var` 宣告變數的語法如下：

```javascript
var gameName = "IT Help 2019";
var gamer = "OneJar", topic = "Something about JavaScript", progress = 7;
var isFinished; // A variable declared without a value will have the value **undefined**.
```

> *作者閒聊：希望 `isFinished` 能順利被賦值(遠目)*

要點懶人包：

* 使用 `var` 關鍵字來宣告變數。
* 可只用一次 `var` 一次宣告多個變數，用逗號 (`,`) 區隔。
* 宣告的同時可進行初始化，也就是賦值。
* 若沒有初始化，則預設值會是 `undefined`。

以上沒什麼問題。

但 `var` 有 3 個缺點，或稱為不足之處。

## 傳統 `var` 有什麼缺點？

### 語法允許重複宣告 (Re-Declaring)

例如以下例子：

```javascript
console.log(x);     // ReferenceError: x is not defined (註)
var x;
console.log(x);     // undefined
x = 123;
console.log(x);     // 123
var x;
console.log(x);     // 123
```

> 註：以上這段程式如果完整執行，第一個 `console.log(x);` 實際上會印出 `undefined` 而不是 Error，因為變數宣告有 **Hoisting** 效果。關於 Hoisting ，預計後續的文章再作詳細介紹，本篇先單純關注 `var` 的問題。

**即使在同一個作用域，同樣名稱的變數也允許重複宣告**，不會跳出任何錯誤或警告，很容易忽略自己曾宣告過。

值得注意的是，**重複宣告一個變數，並不會重置該變數的值**。

一段很長的程式，我們可能不記得前面宣告過同名的變數，後面再次宣告時往往當成第一次宣告，容易疏忽造成小 Bug。

例如以下的情境：

```javascript
var name = 'OneJar';
.........
.........
.........
var name;
while(true){
    if( name === undefined ){
         console.log('The first time to execute.');
    }
    ............
}
```

**允許重複宣告**不僅沒有用處，還容易造成問題。

> 一個較好且普遍的程式編寫習慣：將需要宣告的變數集中在該作用域的一開始，並賦予初始值。

### 不支援區塊作用域 (Block Scope)

例如我們在 Day6 舉的例子：

```javascript
function myFunc(){
    {
       var n1 = "OneJar";
    }
    console.log("myFunc(): n1=", n1);
}

myFunc();
```

執行結果：

```
myFunc(): n1= OneJar
```

**用 `var` 宣告的變數並不具 Block Scope 效果**。

而 Block Scope 特性在其他程式語言很常見，其他語言開發者轉到 JavaScript 時如果沒注意到這一點，容易造成變數管理上的漏洞。

### 不支援常數 (Constant) 特性

常數 (Constant) 指的是「**固定不變的數值**」。

在程式裡常需要宣告一些變數，但變數裡的值只需要作一次初始化，不需要也不希望在程式執行過程被更改，也就是我們希望這類變數具有常數性質。

例如數學的 PI 是 3.14，如果在程式某個小角落被誤改，就會造成整體運算結果的錯誤。

```javascript
var PI = 3.14;
PI = 1234;
```

但傳統的 `var` 無法做到這項控管，你想怎麼改變值都可以，對整體程式來說就造成風險。

許多其他程式語言對於常數性質的變數都有提供控管機制來提高安全性，一旦誤改，可能編譯階段就能發現，甚至 IDE 在編寫階段就能提醒，減少 Debug 負擔。

例如 Java 使用了 `final` 關鍵字來保護指定變數不能被更改值：

```java
public static void main(String []args){
    final double PI = 3.14;
    PI = 1234;
}
```

執行結果：

```
HelloWorld.java:5: error: cannot assign a value to final variable PI
        PI = 1234;
        ^
1 error
```

而在 C# 裡是使用 `const` 關鍵字：

```
public static void Main()
{
    const double PI = 3.14;
    PI = 1234;  // Compilation error
}
```

不同語言所使用的關鍵字或語法可能不一樣，但目的都是一致：保護常數性質的變數。

## 總結

總結用傳統 `var` 宣告變數的 3 個缺點： 1. 語法允許重複宣告 (Re-Declaring) 2. 不支援區塊作用域 (Block Scope) 3. 不支援常數 (Constant) 特性

以上了解傳統 `var` 的缺點和限制，下一篇文章就可以來看 ES6 的 `let` 和 `const` 如何改善這些不足。

## Referneces

* [W3Schools - JavaScript Variables](https://www.w3schools.com/js/js_variables.asp)


# Day 8：var 掰掰 —— ES6 更嚴謹安全的 let 和 const

上一篇文章介紹了傳統 `var` 關鍵字宣告變數的缺點。

> 懶人包支援： 1. 語法允許重複宣告 (Re-Declaring) 2. 不支援區塊作用域 (Block Scope) 3. 不支援常數 (Constant) 特性

今天就來介紹 ES6 的嬌客—— `let` 和 `const`，看他們究竟有什麼本事，可以改善 `var` 做不到的事。

![](https://i.imgur.com/uNKdWPM.png)\
(Source: [網路](https://3.bp.blogspot.com/_JD7pHOAgHko/SwlLKATP-OI/AAAAAAAACN8/XfqfFC--NMw/s1600/%E5%A6%82%E4%BE%86%E7%A5%9E%E6%8E%8C.jpg))

## let

### let 的語意

如同 `var` 關鍵字代表的語意是「**var**iable」，`let` 的語意代表什麼？

從第一次看到 `let` 關鍵字就很好奇這件事，著實想了很久：到底 `let` 是什麼單字的縮寫、簡稱，還是像 `var` 和 `const` 一樣，取單字的頭幾個字母。

雖然知不知道語意由來，對能不能學會 `let` 用法沒有影響，但有助於體會設計者在設計這個新關鍵字時，所期望賦予的意涵，也是一種小樂趣。

後來實在想不到是什麼單字，網路上一查終於真相揭曉：`let` 指的就是……「**let**」。

```javascript
// Hey JS, please
let 
// this variable
n = 'OneJar'
```

![](https://i.imgur.com/sw3gIzz.jpg)\
(Source: [網路](https://img.ltn.com.tw/Upload/liveNews/BigPic/600_phppqUcwz.jpg))

就是這麼簡單 (昏)。

人有時候思考事情會一股腦地往複雜的方面想，結果往往答案就是這麼單純。

根據[網路討論](https://stackoverflow.com/questions/33090193/linguistic-meaning-of-let-variable-in-programming)，其實 `let` 術語的歷史悠久，早在起源於西元 1958 年、現今第二悠久的高階程式語言 LISP 就有這個關鍵字的使用，甚至可能更早，是我見識短淺罷了 (遮臉)。

那 JavaScript 導入這個關鍵字的使用，設計上賦予了什麼性質？

### 支援 Block Scope

舞棍阿伯終於能夠在 JavaScript 裡正式登場。

![](https://i.imgur.com/PPHpxvt.jpg)\
(Source: [網路](https://i.ytimg.com/vi/GbBdx9uwe4o/maxresdefault.jpg))

Day5、Day6 的文章介紹到何謂 Block Scope，而傳統 `var` 不支援 Block Scope 的效果。

下面直接以範例來示範 `let` ：

```javascript
{
   let x = 2;
   {
        console.log(x); // 2
   }
   console.log(x); // 2
}
console.log(x); // ReferenceError: x is not defined
```

可以看到，**用 `let` 宣告的變數，在該 Block 和其子 Block 內有效**。一旦離開 Block 範圍，這個變數就不被認得。

> 其實不難發現，Function 也是一種比較大的 Code Block，在某種程度上 Function Scope 就像一個比較大的 Block Scope 而已。
>
> 只是對傳統 JavaScript 來說 Function Scope 和 Block Scope 有不同的支援度，因此為了避免理解混亂，在介紹上直接將這兩者視作不同的 Scope 種類。

### 禁止同一層 Block 重複宣告變數

使用 `let` 宣告變數的話，在同一層 Block 內不能重覆宣告變數，會拋錯提示：

```javascript
{
   let x = 10;
   let x = 2; // SyntaxError: Identifier 'x' has already been declared
}
```

即使另一個宣告是用比較寬鬆的 `var` 也不行：

```javascript
{
   var x = 10;
   let x = 2; // SyntaxError: Identifier 'x' has already been declared
}
```

但**一個 Block 內的子 Block 允許再宣告同名的變數**：

```javascript
{
   let x = 2;
   {
        let x = 10;
        console.log(x); // 10
   }
   console.log(x); // 2
}
```

其實跟前面介紹過 Function 之於 Global 主程式是一樣的邏輯。

Function 就像 Global Scope 中的子 Block，Global 宣告的變數在 Function 內仍有效用；但 Function 還是能宣告同樣名稱的變數，作用在自己的 Scope 範圍裡。

```javascript
function myFunc(){
    var n1 = "Stephen Chow";
    console.log("myFunc(): n1=", n1);
    console.log("myFunc(): n2=", n2);
}

var n1 = "Tom Cruise";
var n2 = "Meryl Streep";
myFunc();
console.log("Global: n1=", n1);
```

執行結果：

```
myFunc(): n1= Stephen Chow
myFunc(): n2= Meryl Streep
Global: n1= Tom Cruise
```

### 不會產生 Global Scope 變數

我們先回顧前面舉過的例子：

```javascript
function myFunc(){
    console.log("myFunc(): n1=", n1);
    console.log("myFunc(): this.n1=", this.n1);
    console.log("myFunc(): window.n1=", window.n1);
}

var n1 = "OneJar";
myFunc();
console.log("Main: n1=", n1);
```

執行結果：

```
myFunc(): n1= OneJar
myFunc(): this.n1= OneJar
myFunc(): window.n1= OneJar
Main: n1= OneJar
```

用 `var` 在主程式內宣告 `n1`，會被視為 Global 變數，將之存放在 Global Object 裡 (在 HTML 裡就是 `window` 物件)。

但如果是用 `let` 宣告：

```javascript
function myFunc(){
    console.log("myFunc(): n1=", n1);
    console.log("myFunc(): this.n1=", this.n1);
    console.log("myFunc(): window.n1=", window.n1);
}

let n1 = "OneJar";
myFunc();
console.log("Main: n1=", n1);
```

執行結果：

```
myFunc(): n1= OneJar
myFunc(): this.n1= undefined
myFunc(): window.n1= undefined
Main: n1= OneJar
```

可以發現 `n1` 變數：

* 主程式和 `myFunc()` 內依然有效。
* **沒有被存放在 Global Object 裡。**

這是一個微妙的差異，因為如果是在主程式和 `myFunc()` 使用，並不會感覺到什麼差異，`n1` 就像 Global 變數一樣。

**但實際上，`n1` 不是 Global 變數，不會被存放在 Global Object 裡，也無法透過 `window` 物件去取得。**

為什麼幾乎一模一樣的程式碼，宣告位置都一樣，只是把宣告方法改成 `let`，就產生這種差異？

**關鍵在於，當使用 `let` 去宣告變數，在執行時，主程式會被 JavaScript 包裝成一個 Function 去執行。**

概念上就像這樣：

```javascript
function MAIN(){
    function myFunc(){
        console.log("myFunc(): n1=", n1);
        console.log("myFunc(): this.n1=", this.n1);
        console.log("myFunc(): window.n1=", window.n1);
    }

    let n1 = "OneJar";
    myFunc();
    console.log("Main: n1=", n1);
}
```

所有的程式都在一個隱藏的 `MAIN()` 裡面，所以產生的任何變數嚴格來說都是 Block Scope 而已 (Function Scope 可視為比較大的 Block Scope)。

這樣的好處是進一步管控了 Global 變數，只能透過 Global Object 去存取，提升程式的嚴謹性和安全性。

## const

`const` 的語意就是常數 Constant，也就是經初始化後，裡面的值不能再變動。

**可以把 `const` 視為 `let` 的常數加強版，`let` 有的特性 `const` 都有，但 `const` 額外多了常數保護的特性。**

### 和 `let` 一樣是 Block Scope + 禁止重複宣告 + 不會產生 Global Scope

用 `const` 宣告的變數，同樣出了 Block 就會失效：

```javascript
{
    const x = 10;
}
console.log(x); // ReferenceError: x is not defined
```

也禁止在同一層 Block 重複宣告同名變數：

```javascript
var x = 10;
const x = 10; // SyntaxError: Identifier 'x' has already been declared
```

### 定義時必須初始化 (Initialization)

由於常數性質不允許後續再作值的更動，因此宣告的同時就要初始化：

```javascript
const x = 10;
```

如果沒作初始化會拋錯提醒：

```javascript
const x; // SyntaxError: Missing initializer in const declaration
```

### 後續不能更改值

也就是除了初始化，後續不能再對這個變數作賦值的動作：

```javascript
const x = 10;
x = 20; // TypeError: Assignment to constant variable.
```

## 總結

總結 `let` 和 `const` 的重點：

### let

* 支援 Block Scope。
* 禁止同一層 Block 重複宣告變數。
* 不會產生 Global Scope 變數。

### const

* 具備 `let` 的所有特性。
* 定義時必須初始化 (Initialization)。
* 後續不能更改值。

### 應避免使用 `var`，改用 `let` 和 `const`

ES6 導入 `let` 和 `const`，改善 `var` 在變數宣告和管制上的不足，讓程式的變數控管可以更加嚴謹，減少出錯的機率。

因此未來撰寫 JavaScript 時，應該全面使用 `let` 和 `const` 來取代 `var`，讓程式碼更加嚴謹安全。

## References

* [\[ES6\] Javascript 開發者必須知道的 10 個新功能](https://medium.com/@peterchang_82818/es6-10-features-javascript-developer-must-know-98b9782bef44)
* [W3Schools - JavaScript Let](https://www.w3schools.com/js/js_let.asp)
* [W3Schools - JavaScript Const](https://www.w3schools.com/js/js_const.asp)
* [Linguistic meaning of 'let' variable in programming](https://stackoverflow.com/questions/33090193/linguistic-meaning-of-let-variable-in-programming)
* [LISP - Wiki](https://zh.wikipedia.org/wiki/LISP)


# Day 9：圖解變數作用域(Scope)

前幾天的文章談到各種等級的 Scope 效果。

> 懶人包支援：
>
> 在 JavaScript 裡，有 3 種等級的作用域： 1. 香港喜劇天王星爺——Function Level Scope 2. 國際巨星阿湯哥——Global Level Scope 3. 住在隔壁號稱歌神的里長阿伯——Block Level Scope (ES6)

不同情境下各種 Scope 如何作用，有時光靠文字描述仍略覺模糊。

尤其同樣在主程式宣告，使用 `let` 和 `const` 產生的變數是 Block Scope，使用 `var` 產生的變數是 Global Scope 這一段，相對不易理解。

俗話說一張圖勝過千言萬語，這篇文章的目標是將不同 Scope 的情境用圖解的方式說明，希望讓 Scope 一目瞭然。

## 圖解之旅行前說明

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

上面這張圖代表我們一般撰寫 JavaScript 程式的程式結構。

我們會在主程式區——正式一點的名稱為**全域執行環境 (Global Execution Context)** ——開始撰寫程式 (意即程式碼不在任何函數內)。

過程中會用到如 `if-else` 或 `for loop`，形成一個個 Block (如 `Block A`、`Block B`)。

漸漸地，我們會把一些程式片段包裝成函數，形成 Function 區塊 (如 `myFunc1()`、`myFunc2()`)，而函數內又可能再形成小 Block。

對應到實際的程式碼，會類似以下：

```javascript
/* Global Execution Context (outside of any function) */
var i;
console.log(i);

// Block A
if(true){
    .......
}

// Block B
for( i = 0 ; i < 5 ; i ++){
    .......
}


function myFunc1(){
    ..........
    // Block C
    if(true){
        ........
    }
    // Block D
    if(true){
        .........
    }    
}

function myFunc2(){
    ..........
    // Block E
    if(true){
        ........
    }
    // Block F
    if(true){
        .........
    }    
}
```

以下會用實際的程式碼情境，配合程式結構圖片來說明各種 Scope 的有效範圍。

## Example 1：宣告在 Function 內 (使用 `var`、`let`、`const` 都一樣)

```javascript
function myFunc(){
    var n1 = "OneJar";
    console.log("myFunc(): typeof n1=", typeof n1, " value=", n1);
}

myFunc();
console.log("Global: typeof n1=", typeof n1); // 這裡 n1 只能印 type 不能印值，否則會拋 `ReferenceError: n1 is not defined`
```

執行結果：

```
myFunc(): n1= OneJar
Global: typeof n1= undefined
```

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

紅色是 `n1` 宣告的地方，淺藍色部分就是 `n1` 的 Scope。

* 基本 Function Scope。
* 只有在自己這個 function 內有效，包含 function 內的子 Block。
* 別的 function 不認得。
* 主程式區也不認得。

## Example 2：宣告在主程式區 (使用 `var`)

```javascript
function myFunc(){
    console.log("myFunc(): n1=", n1);
    console.log("myFunc(): this.n1=", this.n1);
    console.log("myFunc(): window.n1=", window.n1);
}

var n1 = "OneJar";
myFunc();
console.log("Global: n1=", n1);
```

執行結果：

```
myFunc(): n1= OneJar
myFunc(): this.n1= OneJar
myFunc(): window.n1= OneJar
Global: n1= OneJar
```

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

* `n1` 使用 `var` 宣告在主程式區，會存放在 Global Object 裡，屬於 Global 變數。
* 主程式區內的所有子 Block 和函數都認得。

## Example 3：宣告在主程式區 (使用 `let` 或 `const`)

```javascript
function myFunc(){
    console.log("myFunc(): n1=", n1);
    console.log("myFunc(): this.n1=", this.n1);
    console.log("myFunc(): window.n1=", window.n1);
}

let n1 = "OneJar";
myFunc();
console.log("Main: n1=", n1);
```

執行結果：

```
myFunc(): n1= OneJar
myFunc(): this.n1= undefined
myFunc(): window.n1= undefined
Main: n1= OneJar
```

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

* 可和 Example 2 比較。
* **在執行時，主程式區會被 JavaScript 包裝成一個 Function 去執行** (圖中隱藏的 `Main()`)。
* 所以變數 `n1` 不會成為 Global Scope，而是 Function Scope / Block Scope。

## Example 4：賦值給未宣告的變數，所自動產生的全域變數

```javascript
function myFunc(){
    n1 = "OneJar";  // 自動變成一個 Global 變數
    console.log("myFunc(): n1=", n1);
    console.log("myFunc(): this.n1=", this.n1);
    console.log("myFunc(): window.n1=", window.n1);
}

myFunc();
console.log("Global: n1=", n1);
```

執行結果：

```
myFunc(): n1= OneJar
myFunc(): this.n1= OneJar
myFunc(): window.n1= OneJar
Global: n1= OneJar
```

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

* 紫色代表 `n1 = "OneJar"`，也就是沒有宣告就對 `n1` 賦值的地方。
* 雖然賦值的地方是在 function 內，但因為沒有先宣告，JavaScript 的行為會自動將 `n1` 產生為 Global 變數，所以變成 Global Scope。

## Example 5：Global 和 Function 內同時存在同名變數 (使用 `var`、`let`、`const` 都一樣)

```javascript
function myFunc(){
    var n1 = "Stephen Chow";
    console.log("myFunc(): n1=", n1);
    console.log("myFunc(): this.n1=", this.n1);
    console.log("myFunc(): window.n1=", window.n1);
}

var n1 = "Tom Cruise";
myFunc();
console.log("Global: n1=", n1);
```

執行結果：

```
myFunc(): n1= Stephen Chow
myFunc(): this.n1= Tom Cruise
myFunc(): window.n1= Tom Cruise
Global: n1= Tom Cruise
```

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

* 紅色是 `var n1 = "Tom Cruise"`，宣告在主程式區，屬於 Global Scope。
* 綠色是 `var n1 = "Stephen Chow"`，宣告在主程式區，屬於 Function Scope。
* 淺藍色區域，會生效的是紅色的 `n1`。
* 黃色區域，會生效的是綠色的 `n1`。

## Example 6：Block 內使用 `var` 宣告

```javascript
if(true){
   var x = 2;
   {
        console.log(x); // 2
   }
   console.log(x); // 2
}
console.log(x); // 2
```

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

* 傳統 `var` 不支援 Block Scope。
* 若是宣告在主程式區的 Block，會是 Global Scope (如上圖所示)。
* 若是宣告在函數內的 Block 內 (例如 `Block C` 內)，會是 Function Scope。

## Example 7：Block 內使用 `let` 或 `const` 宣告

```javascript
if(true){
   let x = 2;
   {
        console.log(x); // 2
   }
   console.log(x); // 2
}
console.log(x); // ReferenceError: x is not defined
```

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

* 可和 Example 6 比較。
* 使用 `let` 或 `const` 宣告變數，支援 Block Scope 效果。
* 變數 `x` 只會在被宣告的那個 Block 和其子 Block 被認得。

## 總結

作用域 (Scope) 在程式設計裡是滿重要的一個概念。

不同程式語言隨著不同特性，可能有不同的 Scope 類型。

例如 Java 是物件導向語言，在物件的成員變數和函式前面會加上 `public` 或 `private` 等修飾子來宣告作用域，和 JavaScript 的作用域又是不同的運作原理。

不管是使用哪一種語言開發，都應該注意該語言的作用域運作原理。

## References

* [W3Schools - JavaScript Scope](https://www.w3schools.com/js/js_scope.asp)


# Day 10：程式也懂電梯向上？ —— Hoisting

有沒有注意過 JavaScript 裡一個神奇的現象？

比如以下程式：

```javascript
console.log(x);
```

執行結果：

```
Uncaught ReferenceError: x is not defined
```

![](https://i.imgur.com/zhRQGS8.png)\
(Source: [白爛貓貼圖](https://store.line.me/stickershop/product/1265214/?ref=Desktop))

因為使用了根本不存在的變數 `x`，執行時拋出錯誤，非常合理。

為了彌補，我們趕快增加 `x` 的變數宣告：

```javascript
console.log(x);
var x = "OneJar";
```

你程式老師在你後面，他非常火。

![](https://i.imgur.com/8IUOmvs.png)\
(Source: [Youtube](https://www.youtube.com/watch?v=RrzNpZcoaPs))

變數宣告當然是要在使用之前，補在後面有什麼用，一樣會先執行到拋錯的那一行。

執行結果：

```
undefined
```

![](https://i.imgur.com/IAgBBAs.png)\
(Source: [白爛貓貼圖](https://store.line.me/stickershop/product/4337259/?ref=Desktop))

Why？為什麼宣告在使用之後不會拋錯？而且印了個莫名的答案。

我知道了。

![](https://i.imgur.com/Lb9iqZ9.jpg)\
(Source: [網路](https://farm9.static.flickr.com/8716/28345276131_48339620bf_o.jpg))

喂，冷靜。

程式不會變魔術，一定有理可循。

其實背後的原因，就是今天文章要介紹的 **Hoisting**。

## JavaScript 裡的文章置頂效果 —— Hoisting

Hoisting 這個術語在 [MDN](https://developer.mozilla.org/zh-TW/docs/Glossary/Hoisting) 裡翻譯為「提升」，但我覺得這個名詞太抽象，概念上不易理解。

我個人偏好翻成「**宣告置頂**」。

就像生活中網路論壇常見的置頂文章，當一些文章被設定為置頂文，無論你進入討論版的下一步想做什麼，這些置頂文都會優先被看到。

Hoisting 的效果非常類似這樣的概念。

## Hoisting 置頂了宣告的效果

以下是 W3Schools 裡的介紹：

> Hoisting is JavaScript's default behavior of moving all declarations to the top of the current scope.

Hoisting 是 JavaScript 的預設行為，**把所有宣告效果提到當前 Scope 的頂端**。

也就是說，在正式執行程式之前，JavaScript 會先偷跑一個動作——把程式碼中宣告的部分提前到所屬 Scope 的頂端。

## 變數宣告的 Hoisting 效果 (透過 `var` 關鍵字)

例如文章開頭舉的例子：

```javascript
console.log(x);
var x = "OneJar";
```

運作上等同於：

```javascript
var x;
console.log(x);
x = "OneJar";
```

這就是為什麼 JavaScript 變數可以在宣告之前就使用，而不會拋錯。

但我的程式碼明明是 `var x = "OneJar";`，宣告同時就給予初始值，為何印出來的結果是 `undefined` 而非 `"OneJar"`？

## 變數的 Hoisting 效果只有「宣告」的部分，不包含「初始化」(Initializations)

再引用 W3Schools 裡的原文：

> JavaScript Initializations are Not Hoisted.

也就是說，只有宣告的部分會被提升。

這個觀念不那麼直觀，因為不是以一整行的程式碼來看，而是單獨抽出了程式碼中「宣告」的部分。

以 `var x = "OneJar";` 這行程式來說，裡面包含 2 個動作： 1. `var x`：宣告一個變數名叫 `x`。 2. `x = "OneJar"`：將賦值給變數 `x` (撰寫時將宣告和賦值寫在同一行，這個動作也稱為「初始化」)。

**Hoisting 的效果只涵蓋動作 1，不涵蓋動作 2。**

所以這段程式：

```javascript
console.log(x);
var x = "OneJar";
```

經過 Hoisting 後等同於：

```javascript
var x;
console.log(x);
x = "OneJar";
```

而非：

```javascript
var x = "OneJar";
console.log(x);
```

## 函數也有 Hoisting 效果 (透過 `function` 關鍵字)

以下這種寫法一定不陌生：

```javascript
sayHi();

function sayHi(){
    console.log('Hi');
}
```

執行結果：

```
Hi
```

是否曾經覺得奇怪，為什麼函數 `sayHi()` 宣告定義在後面，卻可以提前呼叫？

這也是 Hoisting 效果。

使用 `function` 關鍵字去宣告函數，整個函數定義都會被提到 Scope 最前面。

```javascript
function sayHi(){
    console.log('Hi');
}

sayHi();
```

## 透過變數方式宣告的函數，Hoisting 效果比照變數

定義函數除了直接使用 `function` 做宣告和定義，也允許用「`var` 變數宣告 + `function` 函數定義」的寫法。

但需要注意的是，就像前面提到：Hoisting 效果只有「宣告」的部分，不包含「初始化」。

例如以下例子：

```javascript
console.log( sayHi );
console.log( sayHi() );

var sayHi = function(){
  return "Hi";
};
```

執行結果：

```
undefined
Uncaught TypeError: sayHi is not a function
```

被提升的只有「`var` 變數宣告」的部分，「`function` 函數定義」的部分仍在原本的位置。

運作上的效果就像以下程式碼：

```javascript
var sayHi;

console.log( sayHi );
console.log( sayHi() );

sayHi = function(){
  return "Hi";
};
```

## 使用 `let` 或 `const` 宣告的變數不具備 Hoisting 效果

W3Schools:

> Variables and constants declared with let or const are not hoisted!

Day8 文章介紹到 ES6 導入新的變數宣告關鍵字：`let` 和 `const`。

需要注意到，這兩個關鍵字所宣告的變數不會有 Hoisting 效果。

```javascript
console.log(x);
let x = "OneJar";
```

執行結果：

```
Uncaught ReferenceError: x is not defined
```

### 補充：真相是 `let` 和 `const` 其實也有 Hoisting

感謝邦友 [Caesar](https://ithelp.ithome.com.tw/users/20113117/profile) 提供一篇文章——[我知道你懂 hoisting，可是你了解到多深？](https://blog.techbridge.cc/2018/11/10/javascript-hoisting/?fbclid=IwAR1bZBRYodHNbq3Xo22HsnRoR-uiJEfnXFJmtdR1fKGAutaTqh8FOCQmONo)，才了解其實 `let` 和 `const` 有 hoisting，只是行為不一樣。

例如以下範例：

```javascript
let x = "OneJar";

function test(){
  console.log(x);
  let x;
}
test();
```

如果沒有 hoisting，理論上應該會根據 Scope Chain 找到外面的 `x`，印出 `"OneJar"`。

但實際上的執行結果：

```
Uncaught ReferenceError: x is not defined
```

原因簡單來說，節錄文章的一句話：

> let 與 const 也有 hoisting 但沒有初始化為 undefined，而且在賦值之前試圖取值會發生錯誤。

文章作者花了很多篇幅講解 hoisting 背後的運作原理，方知小小的 hoisting 觀念要深入，細節也是無窮無盡。

## 總結 Hoisting

Hoisting 效果包含: 1. 使用 `var` 的變數宣告。 2. 使用 `function` 宣告的函數與其定義。

Hoisting 效果不包含: 1. 初始化的部分 (Initializations)，例如變數初始值或使用 `var` 宣告的函數定義。 2. ~~使用 `let` 或 `const` 的變數宣告。~~ (實際上有，但行為和 `var` 不一樣)

## References

* [提升（Hoisting） - 術語表| MDN](https://developer.mozilla.org/zh-TW/docs/Glossary/Hoisting)
* [W3Schools - JavaScript Hoisting](https://www.w3schools.com/js/js_hoisting.asp)
* [我知道你懂 hoisting，可是你了解到多深？](https://blog.techbridge.cc/2018/11/10/javascript-hoisting/?fbclid=IwAR1bZBRYodHNbq3Xo22HsnRoR-uiJEfnXFJmtdR1fKGAutaTqh8FOCQmONo)


# Day 11：Strict Mode (嚴謹模式)

JavaScript 在語法的限制上很寬容，這是它容易上手的優點，不會在學習初期就用一堆語法規則打擊你。

但方便和安全永遠是一體兩面。

就像機場安檢，如果太過嚴格，很安全，但旅客會覺得擾民；如果太寬鬆，旅客方便，恐怖份子也容易出現。

Bug 就是程式裡的恐怖份子。

![](https://i.imgur.com/IGsOxho.png)\
(Source: [網路](https://cw1.tw/CW/images/article/C1465977854096.jpg))

JavaScript 語法限制寬鬆，也是它容易產生 Bug 的原罪。

例如未宣告的變數也能使用、使用寫在宣告之前也能被允許 (Day10 文章介紹的 Hoisting 效果)，這些行為其實很常是程式編寫時不小心打錯變數名稱，但 JavaScript 都會很寬容的對待，有時是用其他預設行為來處理，有時是視而不見，而不會拋錯，這對工程師 Debugging 來說並不理想。

難道 JavaScript 不想改善？

有的，就是今天文章要介紹的 **Strict Mode (嚴謹模式)**。

## 什麼是嚴謹模式 (Strict Mode)

嚴謹模式是 ECMAScript 5 新支援的功能，提供開發者語法嚴格、語法受限的模式。

你可以**透過指令 (Directive) 宣告，選擇要讓你的程式在嚴謹模式下執行**。

如果選擇嚴謹模式，很多原本被接受執行的不良語法行為，會變成拋錯。

例如使用未宣告的變數名稱，原本能正常執行：

```javascript
x = 123;
console.log(x);     // 123
```

如果在嚴謹模式執行，會拋出錯誤：

```javascript
"use strict";
x = 123;            // ReferenceError: x is not defined
console.log(x);
```

## 如何使用嚴謹模式

### 透過`"use strict"` 指令宣告

使用嚴謹模式的方法很簡單：**在程式一開始宣告 `"use strict"` 即可**。

例如：

```javascript
"use strict";
x = 123;            // ReferenceError: x is not defined
console.log(x);
```

要注意的是，**`"use strict"` 宣告位置必須在主程式或函數的開頭**，才會被辨識為嚴謹模式。

例如以下的 `"use strict"` 並不會生效：

```javascript
x = 123;
"use strict";
console.log(x);     // 123
```

### "use strict" 的作用範圍 (Scope)

作用範圍會依宣告位置分為兩種： 1. **宣告在主程式開頭**：Global Scope，所有的程式都會在嚴謹模式下執行。 2. **宣告在函數開頭**：Function Scope，只有該函數內的程式會在嚴謹模式下執行。

以下是 Global Scope 的舉例：

```javascript
"use strict";
myFunc();

function myFunc() {
    y = 3.14;   // ReferenceError: y is not defined
}
```

以下是 Function Scope 的舉例：

```javascript
x = 3.14;       // 啦啦啦~ 我不會拋錯
myFunc();

function myFunc() {
   "use strict";
    y = 3.14;   // ReferenceError: y is not defined
}
```

## 瀏覽器對嚴謹模式的支援性

嚴謹模式是 ES5 新增加的功能，除了一些比較老舊的瀏覽器，現代瀏覽器 (modern browsers) 大多可以支援。

> IE9 及以下版本不支援。

那遇到不支援的瀏覽器怎麼辦？會因為不認得 `"use strict"` 而造成整個程式掛掉？

不用擔心，即使是不支援的瀏覽器，只會忽略 `"use strict"` 用一般模式執行，而不會掛掉。

為什麼呢？

> W3Schools: It is not a statement, but a literal expression, ignored by earlier versions of JavaScript.

這是在設計上，為了能夠相容於舊版瀏覽器的一個小巧思。

**`"use strict"` 本質只是一個字串，而不是關鍵字或描述句**。

例如在正常的 JavaScript 程式中，本來就可以接受一個數字實字 (numeric literal) 或字串實字 (string literal) 單獨存在：

```javascript
console.log("JS here");     // "JS here"
3 + 4;                      // (a numeric literal)
"Hello";                    // (a string literal)
```

對 JavaScript 來說，這些值一度產生，但因為沒有被存放到任何變數內，很快就消滅，對程式不會有任何影響 (side effects)。

所以不認得 `"use strict"` 的瀏覽器也只會把 `"use strict"` 當成一個普通的字串實字，不會造成錯誤。

## 既有程式切換到嚴謹模式必須注意

值得注意的是，同樣的語法在嚴謹模式和一般模式下執行，不僅是拋不拋錯的差別，甚至有些程式行為也不一樣。

如果維護的舊程式本來是在非嚴謹模式下執行測試正常，切換到嚴謹模式後可能有些程式會因為行為不同而導致非預期的結果。

想做這樣的切換，建議配合完整的測試來確保程式運作無虞。

## 為什麼要使用嚴謹模式

使用嚴謹模式貌似給自己找麻煩，讓語法的限制變多，到底有什麼好處？

引用 W3Schools 的一句話：

> Strict mode changes previously accepted "bad syntax" into real errors.

**嚴謹模式下，「不適當的寫法」會從原本被默默接受，轉變成拋出實際錯誤。**

拋錯是 Debugging 非常重要的一環，透過拋出錯誤的訊息，可以加快開發者發現 Bug 和診斷 Bug。

但在一般模式的 JavaScript 下，很多不當或不合邏輯的寫法，JavaScript 沒有給予任何錯誤回饋 (Error Feedback)，容易造成潛在 Bug 或 Debugging 的困難。

例如在一般模式下賦值給不可寫入 (non-writable) 的物件屬性 (property)，雖然不會寫入成功，但也不會拋出錯誤提醒，對程式碼的維護也是種干擾。

## 簡單來說，嚴謹模式的好處在於

* 更容易寫出安全、不易出錯的 JavaScript 程式 ("secure" JavaScript)。
* 幫助你的程式碼更乾淨。

## References

* [W3Schools - JavaScript Use Strict](https://www.w3schools.com/js/js_strict.asp)
* [Strict mode - JavaScript | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode)


# Day 12：看 Strict Mode 如何施展「還我漂亮拳」(1)

Day11 的文章介紹 JavaScript 中的嚴謹模式 (Strict Mode) 是什麼、如何使用、為什麼要用，並舉了簡單的例子來示範。

嚴謹模式 (Strict Mode) 就像 JavaScript 中的「還我漂亮拳」，能夠讓原本不夠嚴謹、造成混亂的語法，變得更安全、乾淨。

![](https://i.imgur.com/WqfhHab.jpg)\
(Source: [網路](https://scontent-ort2-1.cdninstagram.com/vp/9bcf3c4d8ed64dd8b5b9ea403dc62d16/5C630A48/t51.2885-15/e35/s480x480/31384596_175641703266886_1657800957565599744_n.jpg))

「不允許使用未宣告的變數」只是嚴謹模式最簡單的應用，事實上嚴謹模式發揮的地方非常多。

接下來會用各種例子來檢視嚴謹模式使用前和使用後的效果。

## 嚴謹模式下你不能做的事

### 1. 不能使用未宣告的變數

* 強化變數的控管，快速發現打錯變數名稱的 Bug。
* 避免不小心產生不必要的 Global 變數 (賦值給尚未宣告的變數，JavaScript 預設行為會產生 Global 變數)。

> 錯誤類型：`ReferenceError: x is not defined`

使用前：

```javascript
x = 3.14;
console.log(x);     // 3.14
```

使用後：

```javascript
"use strict";
x = 3.14;           // ReferenceError: x is not defined
console.log(x);
```

### 2. 不能刪除變數或函數

* 不能對變數或函數使用 `delete` 運算子 (原本對變數或函數作刪除並不會有效果，但也不會拋錯，等於無用的廢 code)
* 但還是可以對物件屬性或陣列元素使用。

> 錯誤類型：`SyntaxError: Delete of an unqualified identifier in strict mode`

使用前：

```javascript
// variable (沒有作用)
var x = 3.14;
console.log( delete x );                    // false
console.log(x);                             // 3.14

// function via declararion (沒有作用)
function f1(){ console.log('Hi 1'); }
console.log( delete f1 );                   // false
console.log(f1);                            // ƒ f1(){ console.log('Hi 1'); }

// function via expression (沒有作用)
var f2 = function() { console.log('Hi 2'); }
console.log( delete f2 );                   // false
console.log(f2);                            // ƒ () { console.log('Hi 2'); }

// array element
var fruits = ["Banana", "Orange", "Apple", "Mango"];
console.log( delete fruits[2] );            // true
console.log(fruits);                        // ["Banana", "Orange", empty, "Mango"]

// object property
var person = {name: 'John', age:18};
console.log( delete person.name );          // true
console.log(person);                        // {age: 18}
```

使用後：

```javascript
"use strict";

// variable
var x = 3.14;
console.log( delete x );                    // SyntaxError: Delete of an unqualified identifier in strict mode.

// function via declararion
function f1(){ console.log('Hi 1'); }
console.log( delete f1 );                   // SyntaxError: Delete of an unqualified identifier in strict mode.

// function via expression
var f2 = function() { console.log('Hi 2'); }
console.log( delete f2 );                   // SyntaxError: Delete of an unqualified identifier in strict mode.

// array element
var fruits = ["Banana", "Orange", "Apple", "Mango"];
console.log( delete fruits[2] );            // true
console.log(fruits);                        // ["Banana", "Orange", empty, "Mango"]

// object property
var person = {name: 'John', age:18};
console.log( delete person.name );          // true
console.log(person);                        // {age: 18}
```

### 3. 函數的參數名稱不能重複

* 一般模式下，函數的參數名稱如果不慎重複，不會有提醒。

> 錯誤類型：`SyntaxError: Duplicate parameter name not allowed in this context`

使用前：

```javascript
function myFunc(p1, p1){
    console.log(p1);
};

myFunc(10, 20); // 20
```

使用後：

```javascript
"use strict";

function myFunc(p1, p1){    // `SyntaxError: Duplicate parameter name not allowed in this context`
    console.log(p1);
};
```

### 4. 不能使用八進制的數字實字 (Octal numeric literals)

* 某些版本的 JavaScript 中，如果數字前頭帶 `0`，會被直譯為八進制。
* 例如：`var x = 010;` 等於十進制的 `8`。
* 一般 JavaScript 實作規範中就強烈建議不要對數字開頭帶 `0` (但一般模式下仍可被 JavaScript 直譯器接受)。
* 嚴謹模式會直接對這樣的語法拋錯。

> 錯誤類型：`SyntaxError: Octal literals are not allowed in strict mode`

使用前：

```javascript
var n = 010;
console.log(n);     // 8
```

使用後：

```javascript
"use strict";

var n = 010;        // SyntaxError: Octal literals are not allowed in strict mode.
console.log(n);
```

### 5. 不能使用八進制的跳脫字元 (Octal escape characters)

* 例如：`var x = "\010";`。

> 註：我個人沒用過這個用法，實際上印出來也只看得到空字串，不知道可以應用在哪。
>
> 錯誤類型：`SyntaxError: Delete of an unqualified identifier in strict mode.`

使用前：

```javascript
var n = "\010";
console.log(n);     // ""
```

使用後：

```javascript
"use strict";

var n = "\010";     // SyntaxError: Octal escape sequences are not allowed in strict mode.
console.log(n);
```

### 6. 不能對唯讀的物件屬性作寫入 (write to a read-only property of objects)

* 在定義物件屬性時，可以設定是否可寫入 (writable)。
* 一般模式下，如果對 `writable:false` 的屬性作賦值動作，不會寫入成功，但也不會報錯 (等於廢 code)。
* 嚴謹模式下會直接拋錯。

> 錯誤類型：`TypeError: Cannot assign to read only property 'articleTarget' of object '#<Object>'`

使用前：

```javascript
var player = {};
Object.defineProperty(player, "nickname", {value:"OneJar", writable:true});
Object.defineProperty(player, "articleTarget", {value:30, writable:false});
console.log(player);            // {nickname: "OneJar", articleTarget: 30}

player.articleTarget = 100;
console.log(player);            // {nickname: "OneJar", articleTarget: 30}
```

使用後：

```javascript
"use strict";

var player = {};
Object.defineProperty(player, "nickname", {value:"OneJar", writable:true});
Object.defineProperty(player, "articleTarget", {value:30, writable:false});
console.log(player);            // {nickname: "OneJar", articleTarget: 30}

player.articleTarget = 100;     // TypeError: Cannot assign to read only property 'articleTarget' of object '#<Object>'
console.log(player);
```

### 7. 不能對 get-only 的物件屬性作寫入

* 一般模式下，如果對只有 getter 的屬性作賦值動作，不會寫入成功，但也不會報錯 (等於廢 code)。
* 嚴謹模式下會直接拋錯。

> 錯誤類型：`TypeError: Cannot set property age of #<Object> which has only a getter`

使用前：

```javascript
var person = {get age() {return 18} };
console.log(person);                    // {}
console.log(person.age);                // 18
person.age = 70;
console.log(person.age);                // 18
```

使用後：

```javascript
"use strict";

var person = {get age() {return 18} };
console.log(person);                    // {}
console.log(person.age);                // 18
person.age = 70;                        // TypeError: Cannot set property age of #<Object> which has only a getter
console.log(person.age);
```

## References

* [W3Schools - JavaScript Use Strict](https://www.w3schools.com/js/js_strict.asp)
* [Strict mode - JavaScript | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode)


# Day 13：看 Strict Mode 如何施展「還我漂亮拳」(2)

## 嚴謹模式下你不能做的事 (續)

### 8. 不能對不可刪除的屬性 (undeletable properties) 使用 `delete` 運算子

* 一般模式下，`delete` 會回傳 `false`，但語法本身仍可以被接受。
* 嚴謹模式下會直接拋錯。

> 錯誤類型：`TypeError: Cannot delete property 'prototype' of function Object() { [native code] }`

使用前：

```javascript
console.log( delete Object.prototype ); // false
```

使用後：

```javascript
"use strict";

console.log( delete Object.prototype ); // TypeError: Cannot delete property 'prototype' of function Object() { [native code] }
```

### 9. 不能使用 `eval` 或 `arguments` 作為變數名稱

* `eval` 和 `arguments` 是 JavaScript 的關鍵字，各自另有用途，拿來作變數名稱容易導致非預期的結果。

> 錯誤類型：`SyntaxError: Unexpected eval or arguments in strict mode`

使用前：

```javascript
function myFunc(){
    return arguments;
}

var arguments = 1500;
console.log(arguments);         // 1500
console.log(myFunc(1, 2, 3));   // Arguments Object [1, 2, 3]
```

使用後：

```javascript
"use strict";

function myFunc(){
    return arguments;
}

var arguments = 1500;           // SyntaxError: Unexpected eval or arguments in strict mode
console.log(arguments);
console.log(myFunc(1, 2, 3));
```

### 10. 不能使用未來的保留字做變數名稱 (cannot use future reserved keywords as variables)

* 有些字眼可能還不是當前 JavaScript 版本支援的關鍵字，但根據程式語言發展的經驗，有些字眼被預期很有機會在未來成為實際有作用的關鍵字。
* 例如 `interface`、`public`、`private`、`package` 等，在其他程式語言都是很重要也很普遍的關鍵字。
* 考慮到既有程式對未來 JavaScript 新版本的移植性 (portable)，這些字眼就不適合用來作為變數名稱。
* 以下是 W3Schools 列出的未來保留字，有些在 ES6 等後續版本已經成真：
  * implements
  * interface
  * let
  * package
  * private
  * protected
  * public
  * static
  * yield

> 錯誤類型：`TypeError: Cannot assign to read only property 'articleTarget' of object '#<Object>'`

使用前：

```javascript
var public = 1500;
console.log(public);    // 1500
```

使用後：

```javascript
"use strict";
var public = 1500;      // SyntaxError: Unexpected strict mode reserved word
console.log(public);
```

### 11. 用 `eval()` 宣告的變數或函數，不能在該 Scope 被語法呼叫使用

* W3Schools 的原文是：For security reasons, eval() is not allowed to create variables in the scope from which it was called.
* 但嚴格來說，並不是 `eval()` 宣告的時候拋錯，而是後續想使用的時候拋錯，而且拋出的錯誤類型是 `ReferenceError`。
* 此外，如果是在 `eval()` 內自己宣告自己使用也沒有問題，後續想用語法去呼叫使用才會拋錯。
* 所以這邊我的理解是：
  * **在嚴謹模式下，`eval()` 所執行的語法會自成一個暫時的 Scope**，在裡面宣告的變數或函數都只屬於這個 Scope。
  * 所以後續用語法呼叫時，不認得該變數或函數，因而拋出 `ReferenceError` 類型的錯誤。

> 錯誤類型：`ReferenceError: x is not defined`

使用前(用於變數)：

```javascript
eval("var x = 123");
console.log(x);         // 123
```

使用後(用於變數)：

```javascript
"use strict";

eval("var x = 123");
console.log(x);         // ReferenceError: x is not defined
```

嚴謹模式下，在 `eval()` 內自己宣告自己使用並沒有問題：

```javascript
"use strict";

var ret = 0;
eval("var n1 = 3, n2 = 6; ret = n1 + n2;");
console.log(ret);   // 9
console.log(n1);    // ReferenceError: n1 is not defined
```

使用前(用於函數)：

```javascript
eval("function myFunc(){ var x = 123; console.log(x); } myFunc();");
myFunc();
```

```
123
123
```

使用後(用於函數)：

```javascript
"use strict";

eval("function myFunc(){ var x = 123; console.log(x); } myFunc();");
myFunc();
```

```
123
ReferenceError: myFunc is not defined
```

### 12. 不能使用 `with` 語法

* 完全禁止 `with` 語法。

> 錯誤類型：`SyntaxError: Strict mode code may not include a with statement`

使用前：

```javascript
var x, y;
with (Math){
    x = cos(3 * PI) + sin(LN10);
    y = tan(14 * E);
};
console.log(x); // -0.2560196630425069
console.log(y); // 0.37279230719931067
```

使用後：

```javascript
"use strict";

var x, y;
with (Math){   // SyntaxError: Strict mode code may not include a with statement
    x = cos(3 * PI) + sin(LN10);
    y = tan(14 * E);
};
console.log(x);
console.log(y);
```

### 13. 全域執行環境內的函數，裡面的 `this` 代表的物件不一樣

* 一般模式下，全域執行環境內的函數裡面的 `this`，指的是 Global Object。
* 嚴謹模式下，全域執行環境內的函數裡面的 `this`，變成 `undefined`。

使用前：

```javascript
function myFunc(){
    console.log(this === window); // true
}
myFunc();
```

使用後：

```javascript
"use strict";
function myFunc(){
    console.log(this === window); // false (`this` is `undefined`)
}
myFunc();
```

## 嚴謹模式下你還是能……

![](https://i.imgur.com/21mdrMB.jpg)\
(Source: [網路](https://puui.qpic.cn/qqvideo_ori/0/l07381q86of_496_280/0))

嚴謹模式下增加了很多規範，目的是讓程式更安全。

不過也不是盡善盡美，還是有遺珠。

### 1. 還是可以重複宣告變數

* 這是一個很基本的不良語法，不過嚴謹模式並沒有加以控管。

```javascript
"use strict";

var x = 10;
var x = 20;
console.log(x);     // 20
```

這部分會建議使用前面文章介紹過的 `let` 或 `const` 來取代 `var`，就可以限制變數重複宣告。

## 總結

嚴謹模式的使用情境相信還有很多，無法在幾篇文章的篇幅內全數囊括。但希望透過這三篇文章的介紹，能對嚴謹模式有一定深度的了解。

當專案規模越來越大，嚴謹的開發模式和程式撰寫方式才能讓專案更容易被多人維護，使用嚴謹模式是個必然的趨勢。

事實上可以注意到，著名的 JavaScript 編譯器 Babel 在將新版 ECMAScript 轉譯成 ES5 時，在你還沒打任何程式碼之前，ES5 的那一欄已經預設好一行程式，就是 `use strict`！

![](https://i.imgur.com/3B5o5VH.png)\
(Source: [Babel REPL](https://babeljs.io/repl))

## References

* [W3Schools - JavaScript Use Strict](https://www.w3schools.com/js/js_strict.asp)
* [Strict mode - JavaScript | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode)
* [with - JavaScript | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/with)
* [【转载】JavaScript中with、this用法小结](https://king39461.pixnet.net/blog/post/361793370-%E3%80%90%E8%BD%AC%E8%BD%BD%E3%80%91javascript%E4%B8%ADwith%E3%80%81this%E7%94%A8%E6%B3%95%E5%B0%8F%E7%BB%93)
* [\[JavaScript\] with的用法](https://stannotes.blogspot.com/2014/10/javascript-with.html)
* [Try it out - Babel · The compiler for next generation JavaScript](https://babeljs.io/repl)


# Day 14：來挖挖恐龍骨 —— with 語法

JavaScript 有一個語法 `with` 似乎相對冷門，比較少看到被使用。

事實上連 [W3Schools 的 JavaScript 教材](https://www.w3schools.com/js/) 都沒有 `with` 語法教學！查了一下網路討論，`with` 曾經也是有人認為很好用的語法。那麼究竟發生什麼事，讓 `with` 像個黑歷史一樣，被 W3Schools 刻意遺忘？

Day13 介紹嚴謹模式 (Strict Mode) 的例子時提到，`with` 語法甚至已經在 ES5 導入嚴謹模式後被禁止使用。

那本篇去研究一個已經被淘汰的語法有什麼意義？

對，語法沒意義，你知道了也不能用 (毆)。

![](https://i.imgur.com/OWsV1lC.png)\
(Source: [豆卡頻道貼圖](https://store.line.me/stickershop/product/1140900/zh-Hant?from=sticker))

這一篇文章的定位確實有點像考古文，瞭解一下 JavaScript 曾有過這個語法。

但想要探討的**不是這個語法怎麼寫，而是它為什麼會被摒棄？**

![](https://i.imgur.com/wWManCT.png)\
(Source: [網路圖片](https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTiDBrJIeJcp-GaSZI42hma6hssYcdsJsTQMnuntRtUAkZT4vljGA))

一個語法或特性被特地發展出來，然後又被淘汰，一定有其缺點或原因。

**瞭解這些缺點，相當於學到什麼是較被建議的程式思維**。這種概念性上的收穫就像內功一樣，可能不會直接轉化成某種語法的顯性應用，但有助於隱性的程式撰寫思維。

當然，要想探討 `with` 的缺點，首先要先知道語法怎麼寫。

## `with` 的語法

`with` 語法可以為一段程式敘述指定預設物件，用來簡化特定情形下必須撰寫的程式碼量。

語法模板如下：

```javascript
with(<object>){
    // statement
    // ....
    // ....
}
```

以上是 `with` 標準的語法說明。

如果光看這種論文式的說明就知道怎麼用，你一定是百年一見的練武奇才。

![](https://i.imgur.com/2ee8sjQ.jpg)\
(Source: [網路圖片](https://s9.rr.itc.cn/r/wapChange/20164_15_14/a0sisi1715111647352.JPEG))

沒學過如來神掌的人，請跟著導遊繼續往下看實際舉例。

## `with` 使用於內建物件的範例

下面是一個使用 JavaScript 內建數學運算物件 `Math` 的例子：

```javascript
var x = Math.cos(3 * Math.PI) + Math.sin(Math.LN10);
var y = Math.tan(14 * Math.E);
console.log(x);     // -0.2560196630425069
console.log(y);     // 0.37279230719931067
```

可以發現 `Math` 被不斷重複呼叫，使得這一段程式碼看起來很累贅。

這時候可以利用 `with`，讓程式碼變得較簡潔易讀：

```javascript
var x, y;
with (Math){
    x = cos(3 * PI) + sin(LN10);
    y = tan(14 * E);
};
console.log(x);     // -0.2560196630425069
console.log(y);     // 0.37279230719931067
```

可以看到，在 `with(Math){....}` 區塊內，不用再逐一指定每個函式或屬性的呼叫物件，因為已經在 `with` 的小括號內指定了 `Math` 作為預設的呼叫物件。

簡單來說，**當你需要對同一個物件的多個屬性或函式作操作時，就可以使用 `with` 來簡化你的程式碼**。

除了用在內建的 JavaScript 物件，也可以用在自定義的物件上嗎？

當然可以。

## `with` 使用於自訂物件的範例

以下想對自訂物件 `player` 的多個屬性作操作，印出想要的資訊：

```javascript
function showHeroStatus(hero){
    console.log("Name: " + hero.name);
    console.log("Level: " + hero.level);
    console.log("Exp: " + hero.currentExp);
    console.log("You need more " + (hero.nextLevelNeededExp - hero.currentExp) + " Exp points for Level " + (hero.level + 1) + ".");
}

var player = {
    name: "OneJar",
    level: 1,
    currentExp: 50,
    nextLevelNeededExp: 200
};

showHeroStatus(player);
```

執行結果：

```
Name: OneJar
Level: 1
Exp: 50
You need more 150 Exp points for Level 2.
```

可以看到 `showHeroStatus()` 的內容有點囉嗦，不斷重複對 `hero` 的呼叫。

可以利用 `with` 語法讓這段程式碼更簡潔：

```javascript
function showHeroStatus(hero){
    with(hero){
        console.log("Name: " + name);
        console.log("Level: " + level);
        console.log("Exp: " + currentExp);
        console.log("You need more " + (nextLevelNeededExp - currentExp) + " Exp points for Level " + (level + 1) + ".");
    }
}
```

## 撞名會發生什麼事？

看到這邊，應該可以隱約感覺到 `with` 語法的疑慮：**如果我另外有同名的變數會發生什麼事？**

### 1. 變數是在 `with` 區塊之外宣告

```javascript
function showHeroStatus(hero){
    var level = 99;     // Function Scope
    var money = 1300;   // Function Scope

    with(hero){
        console.log("Name: " + name);
        console.log("Level: " + level);     // `with` 預設物件優先
        console.log("Exp: " + currentExp);
        console.log("You need more " + (nextLevelNeededExp - currentExp) + " Exp points for Level " + (level + 1) + ".");
        console.log("Money: " + money);
    }
}
```

執行結果：

```
Name: OneJar
Level: 1
Exp: 50
You need more 150 Exp points for Level 2.
Money: 1300
```

* 在 `with` 之內會以預設物件的屬性為優先。
* 如果該屬性名稱不存在於物件內，會按照作用域鏈 (Scope Chain)的順序，繼續找其他變數定義。
* 在這個例子裡：
  * `level` 會以 `hero.level` 優先。
  * `hero` 沒有 `money` 這個屬性，所以 `money` 會找到 Function Scope 所宣告的 `money` 變數。

### 2. 變數是在 `with` 區塊之內宣告

```javascript
function showHeroStatus(hero){
    with(hero){
        var level = 99;     // The Same Block

        console.log("Name: " + name);
        console.log("Level: " + level);
        console.log("Exp: " + currentExp);
        console.log("You need more " + (nextLevelNeededExp - currentExp) + " Exp points for Level " + (level + 1) + ".");
    }
}
```

執行結果：

```
Name: OneJar
Level: 99
Exp: 50
You need more 150 Exp points for Level 100.
```

* 會以**在 `with` 之內的宣告變數為優先**。
* 在這個例子裡：
  * 呼叫 `level` 時，會以區塊內的 `var level = 99` 優先。

### 3. 變數是在 `with` 區塊之外宣告，但在 `with` 區塊之內被使用

```javascript
function showHeroStatus(hero){
    var level = 99;     // Function Scope

    with(hero){
        level = 70;

        console.log("Name: " + name);
        console.log("Level: " + level);
        console.log("Exp: " + currentExp);
        console.log("You need more " + (nextLevelNeededExp - currentExp) + " Exp points for Level " + (level + 1) + ".");
    }
}
```

執行結果：

```
Name: OneJar
Level: 70
Exp: 50
You need more 150 Exp points for Level 71.
```

* 雖然 `level` 宣告在 `with` 區塊之外，而 `hero` 有 `hero.level` 這個屬性名稱，但在 `with` 之內還是先找到變數 `level`。

## 總結 `with` 作用域的優先順序

當遇到一個呼叫名稱時 (例如 `level`, `money`)： 1. 先從 `with` 區塊內尋找，有使用過即可，不必是在區塊內宣告的變數。 2. 如果找不到，再從預設物件 (例如 `hero`) 的屬性去找同名稱的屬性。 3. 如果以上都找不到，再根據一般的作用域鏈 (Scope Chain) 繼續往外找。

## `with` 的風險

從上面的例子應該可以體會到，**使用 `with` 固然可以節省一點程式碼，但對於程式的作用域運作可能造成混亂**。

例如上面的例子 1 和 3，`level` 都是宣告在 `with` 之外，卻因為是否曾經在 `with` 之內被使用而有不同行為，這對於程式維護安全性來說並非好事。

尤其從現實專案風險的角度來看： 1. 使用 `with` 能節省的程式碼量可能有限；而且簡化程式碼通常屬於「能做到最好，但不能危害到程式運作」的加分項目。 2. 如果使用 `with` 不慎，造成程式運作的行為不同，屬於「一旦發生，會危害到程式運作」的問題。

專案最大的課題就是權衡 (trade-off)。從以上兩點來看，毫無疑問第 2 點對專案的殺傷力遠大於第 1 點，使用 `with` 所得到的效益可能遠不及它隱含的風險，因此 `with` 的摒棄是可以被理解。

## `with` 掰掰～

如前面文章介紹到，在 ES5 導入的嚴謹模式已經禁止 `with` 語法的使用。

[MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/with) 建議：

> Using with is not recommended, and is forbidden in ECMAScript 5 strict mode. The recommended alternative is to assign the object whose properties you want to access to a temporary variable.

如果單純希望程式碼可以再簡潔一點，MDN 的建議是「將需要重複呼叫的物件暫存於一個名稱簡短的變數」就好。

例如下面這樣：

```javascript
"use strict";
var m = Math;
var x = m.cos(3 * m.PI) + m.sin(m.LN10);
var y = m.tan(14 * m.E);
console.log(x);     // -0.2560196630425069
console.log(y);     // 0.37279230719931067
```

## References

* [with - JavaScript | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/with)
* [【转载】JavaScript中with、this用法小结](https://king39461.pixnet.net/blog/post/361793370-%E3%80%90%E8%BD%AC%E8%BD%BD%E3%80%91javascript%E4%B8%ADwith%E3%80%81this%E7%94%A8%E6%B3%95%E5%B0%8F%E7%BB%93)
* [\[JavaScript\] with的用法](https://stannotes.blogspot.com/2014/10/javascript-with.html)


# Day 15：this 關鍵字 (1)

> 今天驟聞武俠大師金庸查先生逝世的消息，身為一位超過二十年的金迷，實在難以表達心中的難過。在此偷渡對一代文學大師的懷念，聊表追思，紀念這個對華文世界影響至深之偉人殞落的日子：金庸武俠，永垂不朽，緬懷再三，一路好走。—— 2018.10.30

`this` 是一個特殊的關鍵字，代表著一個物件，在很多程式語言都可以看到這個設計。由於不同程式語言有各自的特性， `this` 的運作方式也不盡相同。

那在 JavaScript 裡，`this` 指什麼呢？

來看看 W3Schools 的說明：

> * In a method, this refers to the **owner object**.
> * Alone, this refers to the **global object**.
> * In a function, this refers to the **global object**.
> * In a function, in strict mode, this is **undefined**.
> * In an event, this refers to **the element that received the event**.
> * Methods like call(), and apply() can refer this to **any object**.

JavaScript 的 `this` 一下是物件本身，一下是 Global 物件，一下是 `undefined`，一下還可以是任何物件！

![](https://i.imgur.com/RqTPySJ.png)\
(Source: [網路圖片](https://pic.pimg.tw/kenmy/1380581708-2393697494_n.jpg))

`this` 是程式語言中很重要的部分，誤判 `this` 所代表的物件會直接對程式的運作造成影響。所以本篇文章就來好好弄清楚 JavaScript 的 `this`。

## `this` 的簡單範例

照例先上一個基本款的情境範例：

```javascript
var ironMan = {
    firstName: "Tony",
    lastName : "Stark",
    getFullName : function() {
        return this.firstName + " " + this.lastName;
    }
};

console.log(ironMan.getFullName()); // "Tony Stark"
```

這是一個典型的 `this` 應用例子，對物件進行物件導向風格的操作。上面這個例子看起來很好懂，那 JavaScript 的 `this` 到底複雜在哪？

## JavaScript 的 `this` 很複雜？

像 `Java`、`C#` 等基於類別的物件導向語言 (Class-based Object-oriented Languages)，由於語法規範極為嚴謹，大多數情境可以從語彙範圍 (Lexical Scope) —— 也就是看語法上定義在哪個地方來判斷。

但 JavaScript 的 `this` 關鍵字運作與其他語言不同。具體來說，**JavaScript 的 `this` 不是看定義的語彙位置，而是根據執行當下誰擁有這段程式碼**。

> W3Schools: `this` has different values depending on where it is used. The JavaScript `this` keyword refers to the object it belongs to.

換句話說，**不是看該屬性或函式被定義在哪個物件內，看的是執行當下被誰呼叫**。

JavaScript 的語彙定義上沒有像類別那樣完整明確的物件界線，加上語法結構設計上很寬鬆，衍生出各種複雜情境。

此外，不同情境下，`this` 運作的機制也不同，例如：

* 全域執行環境 (Global Context) 下和函數執行環境 (Function Context) 下不同。
* 在嚴謹模式 (Strict Mode) 與一般模式下也可能有所不同。

![](https://i.imgur.com/lcqSy7g.png)\
(Source: [網路圖片](https://colorfulblanche.com/wp-content/uploads/2018/01/%E6%8A%95%E5%BD%B1%E7%89%8713-1024x576.png))

理論描述太抽象，接下來會試圖用各種實際的例子來探討 `this` 的運作。

## 全域執行環境下 (Global Context)

全域環境下比較單純。

> MDN: In the global execution context (outside of any function), this refers to the global object whether in strict mode or not.

* **`this` 在所有函式以外的全域執行環境下，會被當作全域物件，無論是否處於嚴謹模式**。
  * HTML 中就是 `window` 物件。
  * Node.js 中就是 `global` 物件。

例如在全域執行環境下直接印出 `this` 物件：

一般模式：

```javascript
console.log(this); // window object
```

嚴謹模式：

```javascript
"use strict";

console.log(this); // window object
```

## 函數執行環境下 (Function Context)

函數環境下可能遇到的情境就複雜了。

> MDN: Inside a function, the value of this depends on how the function is called.

**在函數內的 `this` 值取決於該函數如何被呼叫。**

就先記著一個大原則：**看是誰呼叫的**，然後用這個原則來看各種情境下 `this` 運作的例子。

### 1. 物件函式 (As an Object Method)

> `this` 物件：呼叫者本身。

這應該是最常見、也相對好理解的情境，也就是 Object Method Binding。

在物件函式的用法，嚴謹模式或一般模式都是一樣的執行結果。

但同樣是物件函式的用法，也有很多種語法情境。

#### 1.1. 函數被定義在物件之內

下面例子中 `player` 物件有 2 個函式，其中一個直接在函數內回傳 `this`，藉此來觀察當下的 `this` 值：

```javascript
var player = {
  name: 'OneJar',
  getName: function() {
    return this.name;
  },
  whatsThis: function() {
    return this;
  },
};

console.log(player.getName());      // "OneJar"
console.log(player.whatsThis());    // {name: "OneJar", getName: ƒ, whatsThis: ƒ}    # `player` object
```

* 執行的函數是 `player.whatsThis` 和 `player.getName` 所指向的函數。
* 呼叫者是 `player`。
* 因此函數內的 `this` 指的是 `player`。

這個例子理解上應該沒什麼困難，很單純的呼叫物件函式用法，加上函數定義就在物件內，判斷上很簡單。

如果函數定義不在物件內呢？

#### 1.2. 借用函數 (函數被定義在物件之外)

函數不是物件本身自己定義，而是指向別人的函數，就像跟別人借用函數一樣。

那在函數裡的 `this`，會是物件本身，還是別人？

是不是開始有趣了。

來看下面的例子：

```javascript
var getName = function() {
    return this.name;
};
var whatsThis = function() {
    return this;
};

var player = { name: 'OneJar' };
player.f1 = getName;
player.f2 = whatsThis;

console.log(player.f1());     // "OneJar"
console.log(player.f2());     // {name: "OneJar", getName: ƒ, whatsThis: ƒ}    # `player` object
```

* `getName()` 和 `whatsThis()` 函數都不是定義在 `player` 內。
* 但執行當下，`player` 是呼叫者，被視為那段程式碼的擁有者，因此 `this` 仍是 `player`。

**函數是否為物件本身的語彙構件無所謂，誰是最直接的呼叫才是最重要的**。

#### 1.3. 物件的屬性物件的函式

物件內的屬性可以是另一個物件，另一個物件也可以有自己的函式，那這時候的 `this` 是誰？

例如以下例子：

```javascript
var getName = function() {
    return this.name;
};

var player = {
  name: 'OneJar',
  f: getName,
  pet: {
    name: 'Totoro',
    f: getName,
  }
};

console.log(player.f());      // "OneJar"
console.log(player.pet.f());  // "Totoro"
```

* `player` 物件擁有另一個物件 `pet`，而 `player` 和 `pet` 都借用 `getName()`。
* `player.f()` 的呼叫者是 `player`。
* `player.pet.f()` 的呼叫者是 `player.pet`。

這裡的原則沒有變，一樣看**誰是呼叫者，誰就是 `this`**。

只是不要混淆，`player.pet.f()` 的呼叫者是 `player.pet` 而不是 `player`。

## References

* [W3Schools - The JavaScript this Keyword](https://www.w3schools.com/js/js_this.asp)
* [this - JavaScript | MDN](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Operators/this)
* [#Javascript：this用法整理 | 英特尔® 软件](https://software.intel.com/zh-cn/blogs/2013/10/09/javascript-this)
* [JavaScript 語言核心（11）this 是什麼？ by caterpillar | CodeData](https://www.codedata.com.tw/javascript/essential-javascript-11-what-is-this/)


# Day 16：this 關鍵字 (2)

## 函數執行環境下 (Function Context) (續)

### 2. 簡易呼叫 (Simple Call)

> `this` 物件：
>
> * 一般模式下：Global 物件。
> * 嚴謹模式下：`undefined`

簡易呼叫指的是函數被單獨呼叫，前面沒有帶任何呼叫物件的情境。

例如這樣的語法：

```javascript
myFunc();
```

當函數被單獨呼叫，**無論呼叫地點在全域環境還是函數環境內，此時執行這段程式的預設綁定擁有者是 Global 物件**。

需要注意的是，**這是指一般模式下的行為，一定會設定一個綁定物件**，因為程式碼沒有指定呼叫物件，Global 物件就被推派出來當預設綁定者。

但基於安全性上的考量，**嚴謹模式下不會作呼叫物件的強制給予**，也就是說不會預設綁定 Global 物件當呼叫物件，程式碼沒指定就當沒有，因此 `this` 會是 `undefined`。

> 節錄 W3Schools 的原文：
>
> * When used alone, the owner is the Global object.
> * The Global object (the owner of the function) is the default binding.
> * Strict mode does not allow default binding.

根據函數被呼叫的地點不同，有幾種情境。

#### 2.1. 全域環境 (Global Context) 下定義函數 & 呼叫函數

一般模式下，`this` 會綁定 Global 物件，在 HTML 環境裡就是 `window` 物件：

```javascript
function f1(){
  return this;
}

console.log( f1() ); // `window`
```

在嚴格模式下，`this` 不會作預設綁定，會是 `undefined`：

```javascript
"use strict";

function f1(){
  return this;
}

console.log( f1() ); // undefined
```

但其實對簡易呼叫來說，在哪裡被定義和呼叫都不重要，再看下一個例子會更明白。

#### 2.2. 內部函數 (Inner Functions)

內部函數是指在 A 函數內定義一個 B 函數，然後在 A 函數裡呼叫 B。

例如以下例子 (一般模式)：

```javascript
var x = 10;
var obj = {
    x: 20,
    f: function(){
        console.log('Output 1: ', this.x);
        var foo = function(){ console.log('Output 2: ', this.x); }
        foo();
    }
};

obj.f();
```

執行結果：

```
Output 1:  20
Output 2:  10
```

這個例子是在 `obj.f()` 內再定義一個內部函數 `foo()` ，進行內部呼叫。

如前面所說，`foo()` 沒有指定呼叫物件，`this` 就是 Global 物件，因此 `foo()` 所印出的 `this.x` 值會是全域變數的 `x`，而非 `obj` 的 `x` (Output 2)。

如果這裡想讓 `foo()` 可以取到`obj.x`，可以使用一個變數去儲存執行 `obj.f()` 時的 `this` 物件。

```javascript
var x = 10;
var obj = {
    x: 20,
    f: function(){
        console.log('Output 1: ', this.x);
        var me = this;   // Use a variable to store the `this` object
        var foo = function(){ console.log('Output 2: ', me.x); }  // Access `obj.x` via `me.x`  
        foo();
    }
};

obj.f();
```

執行結果：

```
Output 1:  20
Output 2:  20
```

### 3. HTML 事件處理 (HTML Event Handlers)

> `this` 物件：接受該事件的 HTML 元素 (HTML Element)。

在 HTML 元件的事件 Callback 裡，`this` 就是該事件的 HTML 元素。

例如下面例子，`onclick` 裡的 `this`，指的就是 `<button>` 元素本身。

```markup
<button onclick="console.log(this); this.style.display='none';">
     Click to Remove Me!
</button>
```

### 4. 顯性函數綁定之 bind() 篇 (Explicit Function Binding for bind())

> `this` 物件：新函數物件被指定的綁定物件，也就是 `Function.prototype.bind()` 的第一個參數。

ES5 導入了 `Function.prototype.bind`，可以為一個函數建立一個繼承該函數 prototype 的新函數物件，但綁定一個固定的擁有者。

換句話說，**無論新的函數物件怎麼被呼叫，函數內的 `this` 都會是當初綁定的那個擁有者物件**。

此外，透過 `Function.prototype.bind` 的綁定，一般模式或嚴謹模式是一樣的結果。

#### 4.1. 一般模式下的範例

例如下面的例子：

```javascript
var getFullName = function() {
    return this.firstName + " " + this.lastName;
}

var firstName = "One", lastName = "Jar";
var introIronMan = getFullName.bind( { firstName: "Tony", lastName : "Stark" } );
var introCaptainAmerica = getFullName.bind( { firstName: "Steven", lastName : "Rogers" } );

console.log(getFullName());           // "One Jar"
console.log(introIronMan());          // "Tony Stark"
console.log(introCaptainAmerica());   // "Steven Rogers"
```

上面例子在全域環境 (Global Context) 定義了函數 `getFullName()`：

* 透過簡單呼叫去執行 `getFullName()`，也是上面 2.1 節所舉的情境。
  * 在一般模式下會預設綁定 Global 物件作為 `this` 值。
  * 因此函數內會找到全域變數 `firstName` 和 `lastName`，因而印出 `"One Jar"`。
* 使用 `getFullName().bind()`，分別產生了兩個新的函數物件，繼承了 `getFullName()` 的 prototype，但各自綁定了固定的擁有者物件。
  * `introIronMan` 函數物件綁定了擁有者物件 `{ firstName: "Tony", lastName : "Stark" }` 。
  * `introCaptainAmerica` 函數物件綁定了擁有者物件 `{ firstName: "Steven", lastName : "Rogers" }` 。
* 一樣透過簡單呼叫的形式去呼叫 `introIronMan()` 和 `introCaptainAmerica()`，函數內的 `this` 值會是各自當初綁定的物件 (而非 Global 物件)。

#### 4.2. 嚴謹模式下有同樣的行為

```javascript
"use strict";

var getFullName = function() {
    return this.firstName + " " + this.lastName;
}

var firstName = "One", lastName = "Jar";
var introIronMan = getFullName.bind( { firstName: "Tony", lastName : "Stark" } );
var introCaptainAmerica = getFullName.bind( { firstName: "Steven", lastName : "Rogers" } );

console.log(getFullName());          // TypeError: Cannot read property 'firstName' of undefined
console.log(introIronMan());         // "Tony Stark"
console.log(introCaptainAmerica());  // "Steven Rogers"
```

* 上面執行 `getFullName()` 時，因為是嚴謹模式，`this` 不再預設綁定 Global 物件，因此是 `undefined` (2.1 節情境)。
* `introIronMan()` 和 `introCaptainAmerica()` 不受影響。

#### 4.3. Binding 就像山盟海誓，只有第一次有效

![](https://i.imgur.com/v6ScbGj.png)\
(Source: [網路圖片](https://www.liidda.com.tw/humanity-feelings-images/pic01.jpg))

需要注意的是，**對一個函數物件來說，只有第一次的 Binding 動作有效**。

透過 `Function.prototype.bind` 所建立的新函數物件 A，如果企圖用 `Function.prototype.bind` 再去建立一個新函數物件 B 並綁定新擁有者，因為函數物件 B 繼承了 A 的 prototype，包含當初的綁定者，因此無論是否給予新的綁定對象都沒有用。

但**第二次綁定的當下並不會拋錯，只是沒有效果**，即使在嚴謹模式下也不會 (感覺這也是嚴謹模式的遺珠)。

```javascript
"use strict";

var getFullName = function() {
    return this.firstName + " " + this.lastName;
}

var introIronMan = getFullName.bind( { firstName: "Tony", lastName : "Stark" } );
var introClone = introIronMan.bind();
var introSpiderMan = introIronMan.bind( { firstName: "Peter", lastName : "Parker"} );

console.log(introClone());        // "Tony Stark"
console.log(introSpiderMan());    // "Tony Stark"
```

## References

* [W3Schools - The JavaScript this Keyword](https://www.w3schools.com/js/js_this.asp)
* [this - JavaScript | MDN](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Operators/this)
* [#Javascript：this用法整理 | 英特尔® 软件](https://software.intel.com/zh-cn/blogs/2013/10/09/javascript-this)
* [JavaScript 語言核心（11）this 是什麼？ by caterpillar | CodeData](https://www.codedata.com.tw/javascript/essential-javascript-11-what-is-this/)


# Day 17：this 關鍵字 (3)

## 函數執行環境下 (Function Context) (續)

### 5. 顯性函數綁定之 call()/apply() 篇 (Explicit Function Binding for call()/apply())

> `this` 物件：函數物件被執行時所指定的綁定物件，也就是 `Function.prototype.call()` 或 `Function.prototype.apply()` 的第一個參數。

由於 `apply()` 和 `call()` 幾乎一樣，差別只在於參數的形式不同：

* `call()` 接受一連串獨立的參數。
* `apply()` 接受一組陣列形式的參數。

```javascript
console.log( Math.max.call(null, 40, 100, 1, 5, 25, 10) );      //100
console.log( Math.max.apply(null, [40, 100, 1, 5, 25, 10]) );   //100
```

因此以下範例只用 `apply()` 來示範。

#### 5.1. 和 bind() 的差別

`apply()` / `call()` 和上一節介紹的 `bind()` 非常相似，都是對函數物件做綁定物件的指定，使用的語法形式也很像。

但同樣對某個函數物件 A 使用時， `bind()` 和 `apply()` / `call()` 的差別在於：

* `bind()` 會**建立一個新的函數物件 B，為 B 綁定一個特定物件，然後回傳 B 物件本身。B 會繼承了 A 的原型**。
  * 如果 A 已經有用 `bind()` 綁定過，B 也會繼承相同的綁定物件，無法再綁新的 (前面 4.3 的範例)。
* `apply()` / `call()` 會**執行函數物件 A，指派一個物件作為 `this`，然後回傳函數 A 的執行結果**。
  * 如果 A 已經有用 `bind()` 綁定過，用 `apply()` / `call()` 再指派其他的物件也沒用。

#### 5.2. 綁定物件範例 (一般模式 & 嚴謹模式)

由於一般模式和嚴謹模式的行為一樣，這裡以一般模式示範。

```javascript
var whatsThis = function() {
    return this;
};
var getFullName = function() {
    return this.firstName + " " + this.lastName;
}

var ironMan = { firstName: "Tony", lastName : "Stark" };
var captainAmerica = { firstName: "Steven", lastName : "Rogers" };

console.log(whatsThis.apply(ironMan));          // {firstName: "Tony", lastName: "Stark"}
console.log(getFullName.apply(ironMan));        // "Tony Stark"
console.log(whatsThis.apply(captainAmerica));   // {firstName: "Steven", lastName: "Rogers"}
console.log(getFullName.apply(captainAmerica)); // "Steven Rogers"
```

直接以要執行的函數物件本身 (e.g., `whatsThis`, `getFullName`) 去呼叫 `apply()`，第一個參數就是函數裡的 `this`。

透過這種方式指定函數的綁定物件，也不需要另外建立一個新的函數物件，即插即用。在使用靈活性上，我認為比 `bind()` 方便。

#### 5.3. 對已經被 Binding 過的函數物件無效

上一節提過，`bind()` 就像海誓山盟，一個函數物件只要曾經和某個物件進行綁定就會死心踏地，即使再進行二次 `bind()` 或利用 `apply()` 指派一次性的綁定物件，都不會生效。

```javascript
var getFullName = function() {
    return this.firstName + " " + this.lastName;
}

var ironMan = { firstName: "Tony", lastName : "Stark" };
var spiderMan = { firstName: "Peter", lastName : "Parker"};

var introIronMan = getFullName.bind( ironMan );
var introClone = introIronMan.bind();
var introSpiderMan = introIronMan.bind( spiderMan );

console.log(introIronMan());                    // "Tony Stark"
console.log(introSpiderMan());                  // "Tony Stark"
console.log(introClone());                      // "Tony Stark"
console.log(introClone.apply(spiderMan));       // "Tony Stark"
console.log(introSpiderMan.apply(spiderMan));   // "Tony Stark"
console.log(getFullName.apply(spiderMan));      // "Peter Parker"
```

以下對每個函數物件的綁定狀況逐一解說：

* `getFullName()` 這個函數物件本身沒有綁定過任何物件。
* `introIronMan()` 是透過 `getFullName()` 產生的新函數物件，綁定了 `ironMan`。
* `introClone()` 是透過 `introIronMan()` 產生的新函數物件，雖然語法沒有指定新的綁定物件，但透過繼承，同樣綁定了 `ironMan`。
* `introSpiderMan()` 是透過 `introIronMan()` 產生的新函數物件，雖然語法指定了新的綁定物件 `spiderMan`，但由於繼承了 `introIronMan()` 的綁定關係，因此綁定物件同樣是 `ironMan`。
* `introClone()` 和 `introSpiderMan()` 因為本身函數物件已經存在綁定了 `ironMan` 的關係，即使透過 `apply()` 指派新的物件，執行時函數內的 `this` 永遠仍是 `ironMan`。
* `getFullName()` 因為沒有存在任何綁定關係，因此透過 `apply()`，可以成功將函數內的 `this` 指派為 `spiderMan`。

### 6. `this` 指向 `new` 所產生的新物件

> `this` 物件：`new` 所產生的新物件。

當對函數使用 `new` 關鍵字來產生一個物件，該物件會形成自己的環境 (Context)，例如以下範例：

```javascript
function Hero(n){
    this.exp = n;
};

var h = new Hero(100);
console.log(h);         // Hero {exp: 100}
console.log(h.exp);     // 100
```

* 原本函數內的 `this.exp` 變成新物件 `h` 的屬性。
* 一般模式和嚴謹模式是一樣的行為。

## References

* [W3Schools - The JavaScript this Keyword](https://www.w3schools.com/js/js_this.asp)
* [this - JavaScript | MDN](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Operators/this)
* [#Javascript：this用法整理 | 英特尔® 软件](https://software.intel.com/zh-cn/blogs/2013/10/09/javascript-this)
* [JavaScript 語言核心（11）this 是什麼？ by caterpillar | CodeData](https://www.codedata.com.tw/javascript/essential-javascript-11-what-is-this/)


# Day 18：this 關鍵字 (4)

## 函數執行環境下 (Function Context) (續)

### 7. 回呼函數 (Callback Function) 裡的 this

> `this` 物件：視乎怎麼呼叫 Callback Function。

JavaScript 裡常會需要用到 Callback Function，將某個函數物件 A 當作參數傳進另一個函數 B，由函數 B 決定執行 A 的時機。

這時候就要注意函數 B 是如何去呼叫函數 A，否則函數 A —— 也就是 Callback Function —— 裡面的 `this`，很可能不是你所預期的對象。

#### 7.1. 簡單呼叫 Callback Function (一般模式)

今天我有一個 `hero` 物件，存在一些屬性，例如 `name`，我想透過 Callback 的方式去控制 `hero` 物件，我只在 `hero` 物件裡實作一個 `act()`，負責執行 Callback Function。而 Callback Function 的內容由其他人提供。例如以下：

```javascript
var name = "Hi I am Global";

function sayHi(){
  return this.name;
}

var hero = {
  name: "Hi I am a Hero",
  act: function(cbk){
    return cbk();
  }
};

console.log( sayHi() );           // Hi I am Global
console.log( hero.act(sayHi) );   // Hi I am Global
```

* `hero.act(sayHi)` 就是一個 callback 的用法，`sayHi` 就是 callback function。
* 在 `hero.act()` 裡，採用**簡單呼叫的方式**來執行 callback function。
* 根據昨天介紹的「 2.簡易呼叫 (Simple Call) 」，函數裡的 `this` 會是 Global 物件，所以 `hero.act(sayHi)` 回傳的結果是 Global 變數的 `name`。

#### 7.2. 簡單呼叫 Callback Function (嚴謹模式)

```javascript
"use strict";

var name = "Hi I am Global";

function sayHi(){
  return this.name;
}

var hero = {
  name: "Hi I am a Hero",
  act: function(cbk){
    return cbk();
  }
};

console.log( sayHi() );           // TypeError: Cannot read property 'name' of undefined
console.log( hero.act(sayHi) );   // TypeError: Cannot read property 'name' of undefined
```

* 如果在嚴謹模式下，透過簡單呼叫，函數裡 `this` 會是 `undefined`，無法執行 `this.name`，會發生錯誤。

但上述的範例，我想達到的效果是 `hero.act(sayHi)` 可以回傳物件 `hero` 自己的 `name` 值，我該怎麼做？

#### 7.3. 用 apply() / call() 將物件本身傳入 Callback Function

如果想把物件本身帶入 Callback Function 裡的 `this`，就要用 `apply()` / `call()`。

```javascript
var name = "Hi I am Global";

function sayHi(){
  return this.name;
}

var hero = {
  name: "Hi I am a Hero",
  act: function(cbk){
    return cbk.apply(this); // 將物件本身傳入 Callback Function
  }
};

console.log( sayHi() );           // Hi I am Global
console.log( hero.act(sayHi) );   // Hi I am a Hero
```

## 總結 this

### 要判斷 this 是誰，就看是誰呼叫

`this` 出現的位置有可能在：

* 全域執行環境下 (Global Context)
* 函數執行環境下 (Function Context)

但全域執行環境下 (Global Context) 遇到的機率相對低，而且十分單純，`this` 就是 Global 物件本身。

比較容易發生問題的是函數執行環境 (Function Context)，遇到的情境可能千變萬化，過程可能讓人很混淆。

記得一個大原則：**看呼叫時的物件是誰**。

**JavaScript 的 `this` 不是看定義的語彙位置，而是根據執行當下誰擁有這段程式碼，也就是看誰呼叫的**。

但要注意的是，簡單呼叫 (Simple Call) 的情形下，一般模式和嚴謹模式會有不同的行為。

### 函數執行環境下，判斷 this 的公式秘笈

所有人都知道，寫程式就像學數學一樣，不建議背誦，而是去理解背後原理 (話說回來，比樂透可能性還多的語法組合，企圖用背的也很不科學)。

但當你確定你已經理解原理之後，我不反對用一些類似口訣或公式筆記的方式來輔助記憶，幫助快速回憶。畢竟如果每次寫程式要用到時，都從頭推導，那效率是不現實的。

曾有人做過實驗，讓大學數學教授和高中生一起做同一份高中數學考券，結果高中生輕輕鬆鬆在時間內完成，數學教授卻沒有寫完。是因為數學教授的程度比高中生差嗎？

當然不是。

數學教授每一個題目都知道背後原理，給他足夠的時間推導，他每一題都能完美解答。但考試時間是有限的，對那些高中生來說，這些題目範圍是他們非常熟悉，幾乎看到題目腦海就浮現解法。這代表那些高中生不理解背後原理嗎？不，他們也是經過理解的過程學到這些題目的解法，但為了應付嚴峻的考試，他們用各種方法來加速遇到題目的解決速度，例如解題口訣、公式表。

現實中的專案開發也有同樣情境。專案開發通常有時程限制，為了減少每次回憶的時間，相信很多工程師都有自己的私藏筆記，這些筆記就是你理解的精華，用來幫助自己快速回想，或減少從頭推導的時間。

前面舉了非常多情境，整體歸納下來，大致上不出以下公式的範圍：

| 呼叫方式                | 模式   | this 所指的物件                                |
| ------------------- | ---- | ----------------------------------------- |
| obj.method()        | 不限   | 該 obj 物件                                  |
| function()          | 一般模式 | Global 物件                                 |
| function()          | 嚴謹模式 | `undefined`                               |
| 透過 apply() 或 call() | 不限   | 第一個參數的物件 (若第一個參數是 `null`，則視同「function()」) |

* 以上指的都是未經 `bind()` 綁定而來的函數物件
* 透過 `bind()` 產生的函數，不管呼叫方式為何，`this` 都指向當初 `bind()` 所綁定的物件。

### 小補充：`this` 不是變數

這一點我想大家都清楚，只是作為 `this` 介紹完整性的一個小補充。

正如最一開始所說，`this` 是一個關鍵字，不是變數，所以不能改變 this 的值，例如企圖這樣：

```javascript
this = { name: "OneJar" };
```

## References

* [W3Schools - The JavaScript this Keyword](https://www.w3schools.com/js/js_this.asp)
* [this - JavaScript | MDN](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Operators/this)
* [#Javascript：this用法整理 | 英特尔® 软件](https://software.intel.com/zh-cn/blogs/2013/10/09/javascript-this)
* [JavaScript 語言核心（11）this 是什麼？ by caterpillar | CodeData](https://www.codedata.com.tw/javascript/essential-javascript-11-what-is-this/)


# Day 19：函數定義 (Function Definition) 的 100 種寫法

標題只是嚇嚇你而已 (毆)。

![](https://thumbs.gfycat.com/ScrawnyDisloyalHeifer-size_restricted.gif)\
(Source: [網路圖片](https://thumbs.gfycat.com/ScrawnyDisloyalHeifer-size_restricted.gif))

函數 (Function) 是程式編寫非常重要的一環。

大多數常見程式語言的函數定義都是一套語法格式，頂多加一些修飾子的變化 (例如 `public`、`private`、`static`)，整體語法還是同一套的格式。

但 JavaScript 裡定義函數的語法花樣可多了，不同寫法所定義出來的函數物件 (Function Object) 也各有些微差異，叫人眼花撩亂。

如果沒有特別需求，太多不同寫法只會造成開發團隊 Coding Convention 一致性的困擾，甚至造成不必要的 Bug 風險。

本篇文章試著去統整已知的 JavaScript 函數寫法，比較彼此差異。

## 宣告函數在語法上有 4 種方式

1. 宣告式 (Function Declarations)
2. 匿名表達式 (Function Expressions w/o Function Name)
3. 具名表達式 (Function Expressions w/ Function Name)
4. 建構子式 (Function Constructor)

> ES6 多了第 5 種 —— Arrow Function。

## 宣告式 (Function Declarations)

特點：

* 最普遍標準的寫法。
* 使用 `function` 關鍵字作函數的宣告和定義。
* 具有 Hoisting 效果，會提升到 Scope 頂端。

```javascript
console.log(myFunc);
console.log(myFunc(3, 6));

function myFunc(a, b) {
    return a + b;
}
```

執行結果：

```
ƒ myFunc(a, b) {
    return a + b;
}
9
```

> 關於 Hoisting 可參考 Day10 文章。

## 匿名表達式 (Function Expressions w/o Function Name)

特點：

* 先宣告一個變數，再定義一個函數內容放到該變數裡。
* 此方式定義的函數實際上是匿名函數 (a function without a name)，只是將函數定義的主體存在某個變數裡。
* 變數名稱不等於函數名稱。
* 不具 Hoisting 效果。

```javascript
console.log(myFunc);
// console.log(myFunc(3, 6)); // TypeError: myFunc is not a function

var myFunc = function (a, b) {
    return a + b;
};

console.log(myFunc);
console.log(myFunc(3, 6));
```

執行結果：

```
undefined
ƒ (a, b) {
    return a + b;
}
9
```

## 具名表達式 (Function Expressions w/ Function Name)

特點：

* 和「匿名表達式」十分相似，只差在定義函數內容時，有給予一個函數名稱。
* 定義的函數印出來會有函數名稱(不等於變數名稱)。
* 但**無法直接透過該函數名稱呼叫**，所以該函數名稱基本上沒用。
* 不具 Hoisting 效果。
* **沒有使用的必要**。

> **為何無法直接透過函數名稱呼叫？**
>
> 目前理解： 因為該函數不算正式宣告於此 Scope，對此 Scope 來說不存在該名稱，所以無法直接透過函數名稱呼叫。

```javascript
console.log(myFunc);
// console.log(myFunc(3, 6)); // TypeError: myFunc is not a function

var myFunc = function aaa(a, b) {
    return a + b;
};

console.log(myFunc);
console.log(myFunc(3, 6));
// console.log(aaa); // ReferenceError: aaa is not defined
```

執行結果：

```
undefined
ƒ aaa(a, b) {
    return a + b;
}
9
```

## 建構子式 (Function Constructor)

W3Schools:

> With a built-in JavaScript function constructor called `Function()`。

特點：

* 先宣告一個變數，再用 JavaScript 內建的函數建構子 `Function()` 去定義函數內容，放到該變數裡。
* 用 `Function()` 定義的函數自動被給予函數名稱 `anonymous`，但和「具名表達式」一樣，都**無法直接透過該函數名稱呼叫**。
* 不具 Hoisting 效果。
* **沒有使用的必要**。

```javascript
console.log(myFunc);
// console.log(myFunc(3, 6)); // TypeError: myFunc is not a function

var myFunc = new Function("a", "b", "return a + b");
console.log(myFunc);
console.log(myFunc(3, 6));
```

執行結果：

```
undefined
ƒ anonymous(a,b
) {
return a + b
}
9
```

## 總結

函數的宣告和定義在語法上有 4 種寫法：

| # | 寫法    | Hoisting | 備註     |
| - | ----- | -------- | ------ |
| 1 | 宣告式   | Y        |        |
| 2 | 匿名表達式 | N        |        |
| 3 | 具名表達式 | N        | 沒有必要使用 |
| 4 | 建構子式  | N        | 沒有必要使用 |

這 4 種寫法定義時的寫法不太一樣，但在使用上除了些許細節 (例如 Hoisting)，大致上其實差不多。

其中 1 和 2 是最普遍的寫法；3 和 4 如果沒有特殊需求，沒必要使用。

ES6 多了第 5 種函數寫法：**Arrow Function**。

由於 **Arrow Function** 不只是語法有所不同，還增加了不少使用上的特性，明天的文章將另外專門對 Arrow Function 作介紹。

## References

* [W3Schools - JavaScript Functions](https://www.w3schools.com/js/js_functions.asp)
* [Javascript 開發學習心得 - 函數的多種寫法與應用限制](https://sweeteason.pixnet.net/blog/post/40371736)


# Day 20：ES6 的箭頭函數 (Arrow Functions)

昨天的文章我們介紹到傳統 JavaScript 對於函數定義的語法有 4 種寫法。

> 懶人包支援： 1. 宣告式 (Function Declarations) 2. 匿名表達式 (Function Expressions w/o Function Name) 3. 具名表達式 (Function Expressions w/ Function Name) 4. 建構子式 (Function Constructor)

這 4 種寫法主要差異在於語法和 Hoisting 效果的差別，其餘少數極細微的差異對於函數的使用呼叫並不影響。換言之，同樣的函數內容，用哪一種寫法並不影響函數內的運作。

相較之下，Arrow Functions 就像函數定義的新種族。

箭頭函數 (Arrow functions) 是 ES6 具代表性的新特性之一，語法上的多項簡化是它的特色，因此很多剛接觸 ES6 未深的開發者會誤以為 Arrow Functions 只是函數定義語法的簡寫版。

事實上，**Arrow Functions 所附帶的新特性對於函數內容的運作有著不同的行為**，如果沒弄清楚，單純因為趕流行，把舊函數的語法簡化成 Arrow Functions 的寫法，可能造成程式運作不正確。

本篇文章就來介紹 Arrow Functions。

## Arrow Functions 小檔案

* ECMAScript 2015 (ES6) 導入的新特性。
* 稱為 Arrow Function Expression，或稱 Fat Arrow Functions，最初在 CoffeeScript 的語法中流行。
* 在函數定義的語法上更為簡潔。
* 函數運作行為上和傳統語法所定義的函數有差異，例如 `arguments`、`this`。

## Arrow Functions 語法

### 1. 標準語法

以下是 Arrow Functions 的標準寫法：

```javascript
var add = (n1 ,n2) => {
  return n1 + n2;
};
console.log( add(3, 6) );   // 9
```

和傳統語法比起來：

* 少了 `function` 關鍵字。
* 使用 `=>` 符號來告知這是 Arrow Functions。

根據函數內容和參數數量，還可以進一步簡化。

### 2. 當函數內容只有單行回傳時的簡寫

很常我們的函數內容只做很簡單的動作，就像前面的 `add()` 只有一行負責簡單運算並同時做 `return`。語法上可以進一步簡化：

```javascript
var add = (n1 ,n2) => n1 + n2;
console.log( add(3, 6) );   // 9
```

* 省略函數外殼 `{` `}`。
* 省略 `return` 關鍵字。

### 3. 當只有單一個參數時的簡寫

例如以下範例，`intro()` 只有一個參數 `name`：

```javascript
var intro = (name) => {
    return `Hi, I am ${name}!`;
};
console.log( intro('OneJar') );     // "Hi, I am OneJar!"
```

`intro()` 在參數部分的語法可以進一步簡化，省略小括號，效果一樣：

```javascript
var intro = name => {
    return `Hi, I am ${name}!`;
};
console.log( intro('OneJar') );     // "Hi, I am OneJar!"
```

搭配前面提到的單行回傳簡寫法，整體語法就更顯簡潔：

```javascript
var intro = name => `Hi, I am ${name}!`;
console.log( intro('OneJar') );     // "Hi, I am OneJar!"
```

但記得這是「單一個參數」時的簡寫，若是沒有參數或多個參數，仍必須用標準寫法，例如以下是沒有參數的範例：

```javascript
var sayHello = () => `Hello OneJar!`;
console.log( sayHello() );          // "Hello OneJar!"
```

## Arrow Functions 使用的注意事項

### 1. 沒有 Hoisting 效果

Arrow Functions 是表達式的語法形式，就像前面介紹過的傳統函數中「具名表達式」或「匿名表達式」那樣，函數定義的部分不會被 Hoist。

換言之，**定義必須寫在使用之前**。

```javascript
console.log( sayHello );    //undefined
console.log( sayHello() );  // TypeError: sayHello is not a function
var sayHello = () => `Hello OneJar!`;
```

### 2. 建議使用 `const` 做名稱部分的宣告

雖然前面例子故意都用 `var`，但實際上**建議使用 `const`**。

因為函數表達式應該被視為一個常數的值，而非變數，函數定義這件事並不是一個該變動的東西。

> W3Schools: A function expression is always constant value.

## Arrow Functions 和傳統函數語法的差異

Arrow Functions 不是單純語法簡化而已，也會對函數的運作行為多了些限制或改變。

### 1. 不會產生新的 `arguments` 物件

> 筆者更新：這裡原本寫的是「不能使用 `arguments`」，後來發現這個理解並不精確，因為如果外圍包裝一層傳統函數，作用域內還是會有可用的 `arguments`，因此進行修訂。

Arrow Functions 不會在自己的函數作用域內產生新的 `arguments` 物件，讓函數使用上更嚴謹。

例如以下是傳統函數寫法，雖然在函數定義的上只定義了 `n1` 和 `n2` 兩個參數，但實際上呼叫函數時我可以丟任意個參數進去，而在函數內也可以靠 `arguments` 取得所有參數：

```javascript
const add = function (n1, n2) {
    console.log(arguments);         // Arguments(3) [100, 200, 300]
    return n1 + n2;
};
console.log( add(100, 200, 300) );  //300
```

甚至極端一點，我可以完全不管參數定義什麼：

```javascript
const add = function (n1, n2) {
    return arguments[0] + arguments[1];
};
console.log( add(100, 200, 300) );  //300
```

這讓函數的參數定義非常沒有約束力。看起來好像很自由，但這種寫法很容易導致「包裝與內容物不符合」，會讓程式難以維護。

而 Arrow Functions 改善這一點。在 Arrow Functions 內不會為這個作用域建立一個新的 `Arguments` 物件，因此 `arguments` 不再被認得：

```javascript
const add = (n1, n2) => {
    console.log(arguments); // ReferenceError: arguments is not defined
    return n1 + n2;
};
console.log( add(100, 200, 300) ); // 300
```

無法使用 `arguments`，代表只能取用被定義的參數。雖然依舊無法阻止呼叫端任意亂丟參數進來，但至少會讓函數內容更可控。

但要注意，**Arrow Functions 只是不會產生新的 `arguments`，不代表 `arguments` 變數一定不存在**。

例如以下例子，使用一個傳統函數包裝一個 Arrow Function：

```javascript
function getObj(){
    console.log(arguments);  // Arguments(3) [1, 2, 3]
    return {
        f: () => {
           console.log(arguments);  // Arguments(3) [1, 2, 3]
        }
    };
}

getObj(1, 2, 3).f(4, 5);
```

* 傳統函數 `getObj()` 還是會產生新的 `arguments`。
* 對作用域來說，`f()` 可以使用 `getObj()` 內存在的變數。
* 因此如果在 `f()` 內去使用 `arguments`，會取到的是 `getObj()` 的 `arguments`，要特別注意。

如果使用 Arrow Function 去包裝另一個 Arrow Function：

```javascript
var getObj = () => {
    console.log(arguments);  // ReferenceError: arguments is not defined
    return {
        f: () => {
           console.log(arguments);  // ReferenceError: arguments is not defined
        }
    };
}

getObj(1, 2, 3).f(4, 5);
```

* `getObj()` 和 `f()` 都不會產生新的 `arguments`。
* 因此在 `getObj()` 和 `f()` 內企圖使用 `arguments`，都會得到 `arguments is not defined` 的錯誤。

### 2. `this` 運作行為的不同

前面介紹 `this` 時，提到判斷 `this` 代表什麼物件的大原則：**看呼叫時的物件是誰**。

例如借用函數的例子 (函數被定義在物件之外)，雖然 `whatsThis` 語彙上定義的地方是在 Global Context，但被執行時的呼叫者是 `player`，因此回傳的 `this` 物件是 `player`：

```javascript
var whatsThis = function() {
    return this;
};

var player = {};
player.f = whatsThis;

console.log(player.f() === player);     // true
```

但如果是 Arrow Functions 就不一樣了：

```javascript
var whatsThis = () => {
    return this;
};

var player = {};
player.f = whatsThis;

console.log(player.f() === player);     // false
console.log(player.f() === window);     // true
```

可以發現，同樣的呼叫方式，`this` 不再回傳呼叫者物件，在這個例子變成回傳 Global 物件，是不同的運作行為。

Arrow Functions 的 `this` 究竟怎麼回事，我們會在明天的文章進行介紹。

## 小結

Arrow Functions 重點小結：

* 是 ES6 導入的新特性。
* 在語法上更為簡潔。
* 不是單純語法簡化而已，也會對函數的運作行為多了些限制或改變：
  * 函數執行時不會產生新的 `arguments` 物件。
  * `this` 的運作方式與傳統函數不同。
* 定義的語法必須在使用之前 (不具 Hoisting 效果)。
* 建議使用 `const` 宣告名稱。

## References

* [W3Schools - ECMAScript 6 - JavaScript 6](https://www.w3schools.com/js/js_es6.asp)
* [\[ES6\] Javascript 開發者必須知道的 10 個新功能](https://medium.com/@peterchang_82818/es6-10-features-javascript-developer-must-know-98b9782bef44)
* [ES6,ES7,ES8 · class - easonwang01 - GitBook](https://easonwang01.gitbooks.io/class/es6es7.html)


# Day 21：箭頭函數 (Arrow Functions) 的 this 和你想的不一樣 (1)

前面 Day15 \~ Day18 舉了很多例子來剖析傳統函數在各種情境下的 `this` 物件。

過程雖然眼花繚亂，但有一個大原則：**看呼叫時的物件是誰**。不是看定義的語彙位置，而是根據執行當下誰擁有這段程式碼，也就是看誰呼叫的。

但在 Arrow Functions 卻不是這麼回事，幾乎可以說是完全不同的另一套運作邏輯。

Day20 介紹箭頭函數 (Arrow Functions) 時示範了一個 `this` 的例子，可以發現和傳統函數的 `this` 運作原則大不相同。

這就是為什麼**最好把 Arrow Functions 當成有別於傳統函數的新種族**，在運作本質上他們有顯著的差別。

## Arrow Functions 的 `this` 判斷原則

那在 Arrow Functions 該怎麼判斷呢？

> MDN & W3Schools:
>
> * In arrow functions, this retains the value of the enclosing lexical context's this.
> * Arrow functions do not have their own this.
> * In global code, it will be set to the global object.

**Arrow Functions 的 `this` 和傳統函數的一個重大差異就是看的是語彙位置**。

傳統函數每次呼叫函數，都會建立一個新的函數執行環境 (Function Execution Context)，然後建立一個新的 `this` 引用物件，指向當下的呼叫者。

**而 Arrow Functions 則不會有自己的 `this` 引用物件，呼叫 `this` 時，會沿用語彙環境外圍的 `this`**。

相信這樣還是很模糊，我們一樣用具體的例子來看。

下面會將前幾天探討傳統函數 `this` 的情境範例，換成 Arrow Functions 的狀況，並和傳統函數做比對。

傳統函數的部分由於前幾天的文章已經詳細解析過，可參考 Day15 \~ Day18 的文章，下面就不再重複細節，會很快帶過，著重在 Arrow Functions 部分的解析和對照。

## 1. 物件函式

### 1.1. 函數被定義在物件之內

#### 傳統函數

傳統函數看的是誰呼叫，所以這個例子相對單純，`player.whatsThis()` 的呼叫者就是 `player`：

```javascript
var player = {
  whatsThis: function() {   // normal function
    return this;
  },
};

console.log( player.whatsThis() === player );    // true
```

#### Arrow Functions

`whatsThis()` 使用 Arrow Functions 定義，因此 `whatsThis()` 本身不會有自己的 `this`，而是沿用外圍環境的 `this`。

在這個例子裡，從 `whatsThis()` 再往外一層不在任何 Function Context 內，換言之就是 Global Context。在全域環境裡的 `this` 就是 Global 物件，在 HTML 裡就是 `window`：

```javascript
var player = {
  whatsThis: () => {    // arrow function
    return this;
  },
};

console.log( player.whatsThis() === window );    // true
```

### 1.2. 借用函數 (函數被定義在物件之外)

#### 傳統函數

傳統函數一樣單純看是誰呼叫，`player.f()` 的呼叫者就是 `player`：

```javascript
var whatsThis = function() {   // normal function
    return this;
};

var player = {};
player.f = whatsThis;

console.log(player.f() === player);     // true
```

#### Arrow Functions

雖然是透過 `player.f()` 呼叫，但這邊看的是語彙位置，`whatsThis()` 再往外一層的 `this` 是 Global 物件：

```javascript
var whatsThis = () => {    // arrow function
    return this;
};

var player = {};
player.f = whatsThis;

console.log(player.f() === window);     // true
```

### 1.3. 物件的屬性物件的函式

#### 傳統函數

根據 `obj.method()` 的公式，呼叫者同樣很好辨認：

```javascript
var player = {
  name: 'OneJar',
  f: function() {
    return this;
  },
  pet: {
    name: 'Totoro',
    f: function() {
      return this;
    },
  }
};

console.log(player.f() === player);             // true
console.log(player.pet.f() === player.pet );    // true
```

#### Arrow Functions

雖然呼叫者不同，函數定義的層次也不太一樣，但由於 `player.f()` 和 `player.pet.f()` 語彙位置再往外一層其實都是 Global Context，所以兩個函數回傳的 `this` 都是 Global 物件：

```javascript
var player = {
  name: 'OneJar',
  f: () => {
    return this;
  },
  pet: {
    name: 'Totoro',
    f: () => {
      return this;
    },
  }
};

console.log(player.f() === window);         // true
console.log(player.pet.f() === window );    // true
```

## 2. 簡易呼叫 (Simple Call)

### 2.1. 全域環境 (Global Context) 下定義函數 & 呼叫函數

#### 傳統函數

前面沒有指定呼叫者的狀況，傳統函數在一般模式下是 Global 物件，嚴謹模式下是 `undefined`：

```javascript
var whatsThis = function() {
  return this;
}

console.log( whatsThis() ); // (normal mode) window / (strict mode) undefined
```

#### Arrow Functions

由於看的是語彙位置，往外一層是 Global Context，不管在一般模式或嚴謹模式，`this` 都是 Global 物件：

```javascript
var whatsThis = () => {
  return this;
}

console.log( whatsThis() ); // window
```

### 2.2. 內部函數 (Inner Functions)

#### 傳統函數

```javascript
var x = 10;
var obj = {
    x: 20,
    f: function(){
        console.log('Output#1: ', this.x);
        var foo = function(){ console.log('Output#2: ', this.x); }
        foo();
    }
};

obj.f();
```

執行結果：

```
Output#1:  20
Output#2:  10
```

* 「Output#1」時，呼叫方式是 `obj.f()`，因此 `this` 是呼叫者 `obj` 物件，`this.x` 是 20。
* 「Output#2」時，呼叫方式是 `foo()`，視同簡單呼叫，一般模式下 `this` 是 Global 物件，因此 `this.x` 是 10。

#### Arrow Functions I

如果只有內部函數是 Arrow Function，外部函數仍是傳統函數：

```javascript
var x = 10;
var obj = {
    x: 20,
    f: function(){
        console.log('Output#1: ', this.x);
        var foo = () => { console.log('Output#2: ', this.x); } // arrow function
        foo();
    }
};

obj.f();
```

執行結果：

```
Output#1:  20
Output#2:  20
```

* 「Output#1」所在的函數仍是傳統函數，因此 `this.x` 不變仍是 20。
* 「Output#2」所在的函數變成 Arrow Function，沿用外層的 `this`，其外層就是 `obj.f()`，因此 `this.x` 也是 20。

#### Arrow Functions II

如果外部函數是 Arrow Function，內部函數是傳統函數：

```javascript
var x = 10;
var obj = {
    x: 20,
    f: () => {  // arrow function
        console.log('Output#1: ', this.x);
        var foo = function() { console.log('Output#2: ', this.x); }
        foo();
    }
};

obj.f();
```

執行結果：

```
Output#1:  10
Output#2:  10
```

* 「Output#2」根據呼叫方式是 `foo()`，視同簡單呼叫，一般模式下 `this` 是 Global 物件，因此 `this.x` 是 10。
* 「Output#1」會沿用外層的 `this`，往外找一層是 Global Context，所以 `this` 也是 Global 物件。

#### Arrow Functions III

如果外部函數和內部函數都是 Arrow Function：

```javascript
var x = 10;
var obj = {
    x: 20,
    f: () => {  // arrow function
        console.log('Output#1: ', this.x);
        var foo = () => { console.log('Output#2: ', this.x); } // arrow function
        foo();
    }
};

obj.f();
```

執行結果：

```
Output#1:  10
Output#2:  10
```

* 「Output#1」會沿用外層的 `this`，往外找一層是 Global Context，所以 `this` 也是 Global 物件。
* 「Output#2」沿用外層的 `this`，其外層是 `obj.f()`；而 `obj.f()` 的 `this` 如上面所說，經過沿用後是 Global 物件。

> 可以發現上面第 2 和 第 3 個情境的結果都是 Output#1 和 Output#2 等於 10。雖然他們最後呈現的結果碰巧一樣，但要注意背後的運作原理其實有所差別。

## References

* [W3Schools - ECMAScript 6 - JavaScript 6](https://www.w3schools.com/js/js_es6.asp)
* [W3Schools - The JavaScript this Keyword](https://www.w3schools.com/js/js_this.asp)
* [this - JavaScript | MDN](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Operators/this)
* [#Javascript：this用法整理 | 英特尔® 软件](https://software.intel.com/zh-cn/blogs/2013/10/09/javascript-this)
* [JavaScript 語言核心（11）this 是什麼？ by caterpillar | CodeData](https://www.codedata.com.tw/javascript/essential-javascript-11-what-is-this/)
* [\[ES6\] Javascript 開發者必須知道的 10 個新功能](https://medium.com/@peterchang_82818/es6-10-features-javascript-developer-must-know-98b9782bef44)
* [ES6,ES7,ES8 · class - easonwang01 - GitBook](https://easonwang01.gitbooks.io/class/es6es7.html)


# Day 22：箭頭函數 (Arrow Functions) 的 this 和你想的不一樣 (2)

## 3. 顯性函數綁定 (Explicit Function Binding)

### 3.1. Function.prototype.bind() 篇

#### 傳統函數

`Function.prototype.bind()` 可以為一個函數建立新函數物件，新函數物件會繼承原函數的 prototype，同時任意綁定一個固定的擁有者。

因此以下例子中的 `introIronMan()` 和 `introCaptainAmerica()` 雖然呼叫形式上是簡單呼叫，`this` 會指向自己的綁定物件，而非指到 Global 物件。

```javascript
var getFullName = function() {
    return this.firstName + " " + this.lastName;
}

var firstName = "One", lastName = "Jar";
var introIronMan = getFullName.bind( { firstName: "Tony", lastName : "Stark" } );
var introCaptainAmerica = getFullName.bind( { firstName: "Steven", lastName : "Rogers" } );

console.log(getFullName());           // "One Jar"
console.log(introIronMan());          // "Tony Stark"
console.log(introCaptainAmerica());   // "Steven Rogers"
```

#### Arrow Functions

就像前面一再強調，**Arrow Functions 的 `this` 判斷看的是語彙位置**，因此 `Function.prototype.bind()` 的 Binding 不會發生作用，同樣只會沿用外層的 `this` 物件。

這個例子裡，`getFullName()` 往外一層是 Global Context，不管在一般模式或嚴謹模式，`this` 都是 Global 物件：

```javascript
var getFullName = () => {
    return this.firstName + " " + this.lastName;
}

var firstName = "One", lastName = "Jar";
var introIronMan = getFullName.bind( { firstName: "Tony", lastName : "Stark" } );
var introCaptainAmerica = getFullName.bind( { firstName: "Steven", lastName : "Rogers" } );

console.log(getFullName());           // "One Jar"
console.log(introIronMan());          // "One Jar"
console.log(introCaptainAmerica());   // "One Jar"
```

### 3.2. Function.prototype.apply() / Function.prototype.call() 篇

#### 傳統函數

透過 `apply()` / `call()` 執行某個函數物件，同時指定一個物件作為 `this`，然後回傳函數的執行結果：

```javascript
var whatsThis = function() {
    return this;
};
var getFullName = function() {
    return this.firstName + " " + this.lastName;
}

var ironMan = { firstName: "Tony", lastName : "Stark" };
var captainAmerica = { firstName: "Steven", lastName : "Rogers" };

console.log(whatsThis.apply(ironMan) === ironMan);                // true
console.log(getFullName.apply(ironMan));               // "Tony Stark"
console.log(whatsThis.apply(captainAmerica) === captainAmerica);  // true
console.log(getFullName.apply(captainAmerica));        // "Steven Rogers"
```

#### Arrow Functions

就像 `Function.prototype.bind()` 的 Binding 不會發生作用，`apply()` 和 `call()` 也同樣無效。只會依照語彙位置來判定 `this` 物件。

這個例子裡的 `getFullName()` 和 `whatsThis()` 往外一層都是 Global Context，因此不管在一般模式或嚴謹模式，`this` 都是 Global 物件，而 Global 物件裡並沒有 `firstName` 和 `lastName` 變數，所以印出 `"undefined undefined"`：

```javascript
var whatsThis = () => {
    return this;
};
var getFullName = () => {
    return this.firstName + " " + this.lastName;
}

var ironMan = { firstName: "Tony", lastName : "Stark" };
var captainAmerica = { firstName: "Steven", lastName : "Rogers" };

console.log(whatsThis.apply(ironMan) === window);        // true
console.log(getFullName.apply(ironMan));          // "undefined undefined"
console.log(whatsThis.apply(captainAmerica) === window); // true
console.log(getFullName.apply(captainAmerica));   // "undefined undefined"
```

## 4. 函數作為建構子

#### 傳統函數

將函數當作建構子，透過 new 關鍵字來產生一個物件，該物件會形成自己的環境 (Context)，原本函數內的 `this.xxx` 變成新物件的屬性。例如以下範例：

```javascript
var Hero = function(n){
    this.exp = n;
};

var h = new Hero(100);
console.log(h);         // Hero {exp: 100}
console.log(h.exp);     // 100
```

#### Arrow Functions

Arrow Function 所宣告的函數不能拿來當建構子，也不存在 `this` 的問題。

```javascript
var Hero = (n) => {
    this.exp = n;
};

var h = new Hero(100); // TypeError: Hero is not a constructor
```

## 5. 回呼函數 (Callback Function) 裡的 this

### 5.1. 簡單呼叫 Callback Function

#### 傳統函數

我們會把某函數 A 當作參數傳入函數 B，函數 A 就是 Callback Function。

而傳統函數裡，Callback Function 裡的 `this` 是誰，視乎在函數 B 裡是怎麼呼叫函數 A。如果是最常見的「簡單呼叫」的形式，此時 `this` 在一般模式下就是 Global 物件，嚴謹模式則是 `undefined`：

```javascript
var name = "Hi I am Global";

var sayHi = function(){
  return this.name;
}

var hero = {
  name: "Hi I am a Hero",
  act: function(cbk){
    return cbk();
  }
};

console.log( sayHi() );           // Hi I am Global
console.log( hero.act(sayHi) );   // Hi I am Global
```

#### Arrow Functions I

當函數 A (Callback Function) 是傳統函數，不管函數 B 是傳統函數 (`hero.act1()`) 還是箭頭函數 (`hero.act2()`)，因為 Callback Function 本身是傳統函數，裡面的 `this` 比照傳統函數的判斷方式，也就是看呼叫方式。

由於都是透過簡單呼叫，所以 `this` 在一般模式下是 Global 物件，嚴謹模式是 `undefined`：

```javascript
var name = "Hi I am Global";

var sayHi = function(){
  return this.name;
}

var hero = {
  name: "Hi I am a Hero",
  act1: function(cbk){
    return cbk();
  },
  act2: (cbk) => {    // arrow function
    return cbk();
  }
};

console.log( sayHi() );           // Hi I am Global
console.log( hero.act1(sayHi) );  // Hi I am Global
console.log( hero.act2(sayHi) );  // Hi I am Global
```

#### Arrow Functions II

當函數 A (Callback Function) 是箭頭函數，不管函數 B 是哪一種函數，都是看 Callback Function 本身的語彙位置。

由於 `sayHi()` 沿用外層的 `this`，不管是一般模式或嚴謹模式，`this` 都是 Global 物件：

```javascript
var name = "Hi I am Global";

var sayHi = () => {    // arrow function
  return this.name;
}

var hero = {
  name: "Hi I am a Hero",
  act1: function(cbk){
    return cbk();
  },
  act2: (cbk) => {    // arrow function
    return cbk();
  }
};

console.log( sayHi() );           // Hi I am Global
console.log( hero.act1(sayHi) );  // Hi I am Global
console.log( hero.act2(sayHi) );  // Hi I am Global
```

### 5.2. 用 apply() / call() 將物件本身傳入 Callback Function

#### 傳統函數

透過 `apply()` / `call()` 可以明確地控制函數裡的 `this` 物件是誰：

```javascript
var name = "Hi I am Global";

function sayHi(){
  return this.name;
}

var hero = {
  name: "Hi I am a Hero",
  act: function(cbk){
    return cbk.apply(this); // 將物件本身傳入 Callback Function
  }
};

console.log( sayHi() );           // Hi I am Global
console.log( hero.act(sayHi) );   // Hi I am a Hero
```

#### Arrow Functions I

當函數 A (Callback Function) `sayHi()` 是傳統函數時，受 `apply()` 效果影響：

* 當函數 B 也是傳統函數 (`hero.act1()`) ：`hero.act1()` 自己的 `this` 看呼叫者是誰，也就是 `hero`，所以 `apply()` 將 `hero` 綁定為 Callback Function 的 `this`，因此印出 `"Hi I am a Hero"`。
* 當函數 B 是 Arrow Function (`hero.act2()`) ：`hero.act2()` 自己的 `this` 沿用外層，也就是 Global 物件 (無論一般模式或嚴謹模式)；再透過 `apply()` 將 Global 物件綁定於 `sayHi()` 的 `this`，因此印出 `"Hi I am Global"`。

```javascript
var name = "Hi I am Global";

var sayHi = function(){
  return this.name;
}

var hero = {
  name: "Hi I am a Hero",
  act1: function(cbk){
    return cbk.apply(this); // 將物件本身傳入 Callback Function
  },
  act2: (cbk) => {    // arrow function
    return cbk.apply(this); // 將物件本身傳入 Callback Function
  }
};

console.log( sayHi() );           // Hi I am Global
console.log( hero.act1(sayHi) );  // Hi I am a Hero
console.log( hero.act2(sayHi) );  // Hi I am Global
```

#### Arrow Functions II

由於 `apply()` / `call()` 的綁定效果對 Arrow Function 無效，如果函數 A (Callback Function) `sayHi()` 是 Arrow Function，無論函數 B 是傳統函數 (`hero.act1()`) 或者 Arrow Function (`hero.act2()`)，`sayHi()` 裡的 `this` 都是沿用外層，也就是 Global 物件 (無論一般模式或嚴謹模式)：

```javascript
var name = "Hi I am Global";

var sayHi = () => {    // arrow function
  return this.name;
}

var hero = {
  name: "Hi I am a Hero",
  act1: function(cbk){
    return cbk.apply(this); // 將物件本身傳入 Callback Function
  },
  act2: (cbk) => {    // arrow function
    return cbk.apply(this); // 將物件本身傳入 Callback Function
  }
};

console.log( sayHi() );           // Hi I am Global
console.log( hero.act1(sayHi) );  // Hi I am Global
console.log( hero.act2(sayHi) );  // Hi I am Global
```

> 可以注意到，同樣遇到 Callback Functions 的情境，傳統函數和箭頭函數的判斷原理可說是完全相反：
>
> * 傳統函數：看的是函數 B，也就是呼叫方怎麼呼叫函數 A。
> * 箭頭函數：看的是函數 A，也就是函數自身定義的語彙位置。

## 總結

### 一句話總結傳統函數和箭頭函數在 `this` 判斷上的差別

* 傳統函數：看呼叫時的物件是誰。
* 箭頭函數：看函數本身定義的語彙位置。

### 既然一句話就能講完，吃飽太閒看這麼多範例？

這兩篇文章不厭其煩的將一些情境的範例用 Arrow Function 走過一遍，十分囉嗦。

![](https://i.imgur.com/mxGyGFc.png)\
(Source: [Youtube](https://www.youtube.com/watch?v=wPxXT0nB36I))

其實目的是希望**能不犯了「想當然耳」的毛病**。

前面一大堆情境程式碼，很多情境的差別可能很細微，之所以不厭其煩逐一走過，其實就像一種交叉驗證，反覆驗證歸納出來的規則是否有漏洞，確認真的能夠解釋每個範例的背後原理。

**在理解觀念時，很容易在腦海裡模模糊糊以為自己都懂了，殊不知有些細節並沒有真正理解清楚**。

事實上在寫這個系列文之前，很多觀念我以為我早就熟稔。結果將腦海中的理解化為具體文章的過程，才發現自己在一些細節並不如自以為的了解。

在前面介紹各種範例的過程可以發現，很多範例的 `this` 最後呈現結果雖然一樣，可能都是 Global 物件，但背後形成「`this` 是 Global 物件」的原因並不相同。

有時程式碼 99% 一樣，僅僅是定義時用傳統函數或箭頭函數的差別，就可以讓結果天差地遠。

**如果對於背後細節原理不加分辨，未來一旦換了不同的程式碼情境，很可能就判斷錯誤**。

就像看人家講解程式題目，常常覺得聽起來很容易，以為自己都懂了，一旦輪到自己上戰場，才發現很多細節並沒有理解清楚，換個情境就卡關。

要確保自己的理解沒有死角，最好的方法就是實地走過一次。

事實上可能遇到的程式情境太多了，有太多令人混淆之極的例子可以研究。

不厭其煩地對各種情境動手實作，就是學好程式的不二法門！

## References

* [W3Schools - ECMAScript 6 - JavaScript 6](https://www.w3schools.com/js/js_es6.asp)
* [W3Schools - The JavaScript this Keyword](https://www.w3schools.com/js/js_this.asp)
* [this - JavaScript | MDN](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Operators/this)
* [#Javascript：this用法整理 | 英特尔® 软件](https://software.intel.com/zh-cn/blogs/2013/10/09/javascript-this)
* [JavaScript 語言核心（11）this 是什麼？ by caterpillar | CodeData](https://www.codedata.com.tw/javascript/essential-javascript-11-what-is-this/)
* [\[ES6\] Javascript 開發者必須知道的 10 個新功能](https://medium.com/@peterchang_82818/es6-10-features-javascript-developer-must-know-98b9782bef44)
* [ES6,ES7,ES8 · class - easonwang01 - GitBook](https://easonwang01.gitbooks.io/class/es6es7.html)


# Day 23：ES6 物件實字威力加強版 (Enhanced Object Literals)

今天文章輕鬆一點，來介紹一個簡單的 ES6 新特性：**Enhanced Object Literals**。

## 什麼是物件實字 (Object Literals)

平常我們用大括號 (`{}`) 來建立物件的語法，就稱為物件實字 (Object Literals)。

以下例子是典型的物件實字語法：

```javascript
var obj = {};

var player = {
    name: "OneJar", 
    progress: 23,
    sayHi: function(){
        return "Hello";
    },
    language1: "JavaScript",
    language2: "Java",
    language3: "C"
};
```

**物件實字的語法重點：**

* 用大括號表示。
* 裡面的**屬性 (Properties)** 用**名值對 (name-value pairs)** 表示。
* 多個屬性以逗號 (comma) 分隔。
* 宣告完後，還是可以再增加 Properties 進去。

## ES6 推出物件實字語法的加強版

ES6 支援一些新的語法，讓物件實字能夠更簡潔、靈活。

### 1. 物件屬性初始化的語法簡寫

#### ES5

時常會有這樣的情境：要使用已存在的變數名稱對物件屬性作初始化，而命名習性的關係，變數名稱可能和屬性名稱一模一樣。

好拗口，我們還是來看例子吧：

```javascript
function getPlayerObj(name, progress){
    return {
        name: name,
        progress: progress
    };
}

console.log( getPlayerObj("OneJar", 23) );   // {name: "OneJar", progress: 23}
```

我想把變數 `name` 和 `progress` 作為物件屬性 `name` 和 `progress` 的初始值，變數名稱和屬性名稱碰巧一模一樣。

在 ES5 以前，必須把**屬性名稱**和**進行賦值的變數名稱**都標明清楚。

寫清楚並沒有不好，只是當這種情境越來越多，程式碼看起來可能稍嫌囉嗦。

#### ES6

對於這種「**屬性名稱**和**進行賦值的變數名稱**一模一樣」的情境，ES6 提供了更簡潔的語法：

```javascript
function getPlayerObj(name, progress){
    return {
        name,
        progress
    };
}

console.log( getPlayerObj("OneJar", 23) );   // {name: "OneJar", progress: 23}
```

### 2. 物件函式的語法簡寫

#### ES5

ES5 以前，物件函式的宣告方式就是使用 `function` 關鍵字進行函數的定義：

```javascript
function getPlayerObj(name, progress){
    return {
        sayHi: function(){
           return `Hi, I am ${name}`;
        }
    };
}

console.log( getPlayerObj("OneJar", 23).sayHi() ); // "Hi, I am OneJar"
```

#### ES6

ES6 支援更簡潔的寫法，省略了 `function` 關鍵字和冒號 `:`：

```javascript
function getPlayerObj(name, progress){
    return {
        sayHi(){
           return `Hi, I am ${name}`;
        }
    };
}

console.log( getPlayerObj("OneJar", 23).sayHi() ); // "Hi, I am OneJar"
```

#### 注意！ES6 簡寫語法的函數視為傳統函數

物件函式寫法除了**用 `function` 關鍵字定義傳統函數**，也可以用**箭頭函數 (Arrow Functions)** 的方式定義：

```javascript
function getPlayerObj(name, progress){
    return {
        sayHi: () => {
           return `Hi, I am ${name}`;
        }
    };
}
```

前面幾天的文章，我們談到**傳統函數**和**箭頭函數 (Arrow Functions)** 的差異，兩者的運作行為有很大不同。

**那透過 ES6 簡寫語法所定義的物件函式，行為是傳統函數還是箭頭函數？**

答案是傳統函數。

如何得知？

很簡單，只要一個簡單的測試。

例如箭頭函數不會為自己產生一個新的 `arguments` 物件，如果使用 `arguments` 發生錯誤，就代表是箭頭函數：

```javascript
var player = {
    sayHi: (name) => {
        console.log(arguments); // ReferenceError: arguments is not defined
        return `Hi, I am ${name}`;
    }
};

console.log( player.sayHi("OneJar") );
```

若能正常取到屬於這個函數的 `arguments`，就代表是傳統函數：

```javascript
var player = {
    sayHi(name){
        console.log(arguments); // Arguments(1) ["OneJar"]
        return `Hi, I am ${name}`;
    }
};

console.log( player.sayHi("OneJar") ); // "Hi, I am OneJar"
```

### 3. 具運算性的屬性名稱

#### ES5

ES5 以前的物件實字語法，無法透過運算去定義屬性名稱。

例如無法在程式執行過程，透過運算來動態決定下面 `player` 物件裡的屬性名稱要叫 `language1` 或是 `lng1`：

```javascript
var player = {
    language1: "JavaScript",
    language2: "Java",
    language3: "C"
};

var player = {
    lng1: "JavaScript",
    lng2: "Java",
    lng3: "C"
};
```

當然，你還是可以用以下的寫法去達到目的：

```javascript
var prefix = "lng";
var i = 0;

var player = {};
player[prefix + (++i)] = "JavaScript";
player[prefix + (++i)] = "Java";
player[prefix + (++i)] = "C";

console.log(player); // {lng1: "JavaScript", lng2: "Java", lng3: "C"}
```

只是這種寫法就不是所謂的物件實字而已。

#### ES6

ES6 直接在物件實字的語法內增加可運算的特性，算是提供了另一種語法選擇：

```javascript
var prefix = "lng";
var i = 0;

var player = {
    [prefix + (++i)]: "JavaScript",
    [prefix + (++i)]: "Java",
    [prefix + (++i)]: "C"
};

console.log(player); // {lng1: "JavaScript", lng2: "Java", lng3: "C"}
```

## 總結

ES6 提升了物件實字語法的簡潔性和靈活性，總共有 3 個部分的加強： 1. 物件屬性初始化的語法簡寫 (Shorthand for Initializing Properties) 2. 物件函式的語法簡寫 (Shorthand for Writing Methods) 3. 具運算性的屬性名稱 (Computed Properties and Object Literals)

Enhanced Object Literal 是 ES6 中算滿簡單的新特性，唯一值得特別注意的是：**透過 ES6 簡寫的物件函式，函數行為是傳統函數而非箭頭函數**。

經過前面幾天的介紹，相信對箭頭函數和傳統函數的行為差異已經有一定的概念，能夠很輕鬆地理解為什麼 ES6 簡寫的物件函式是傳統函數，這也是為什麼把本篇主題接在箭頭函數之後才介紹。

## References:

* [Enhanced Object Literals in ES6 - DEV Community](https://dev.to/sarah_chima/enhanced-object-literals-in-es6-a9d)
* [\[ES6\] Javascript 開發者必須知道的 10 個新功能](https://medium.com/@peterchang_82818/es6-10-features-javascript-developer-must-know-98b9782bef44)
* [ES6,ES7,ES8 · class - easonwang01 - GitBook](https://easonwang01.gitbooks.io/class/es6es7.html)


# Day 24：函數呼叫 (Function Invocation) 與立即函數 (Self-Invoking Functions)

Day19 的文章曾介紹傳統函數定義 (Function Definition) 的 ~~100 種~~ 4 種寫法，定義了也得有人呼叫才有用，那今天來介紹函數呼叫 (Function Invocation) 吧。

根據場合，函數有 3 種被呼叫的時機，其中立即函數 (Self-Invoking Functions) 是特別有趣的一種，會花比較多篇幅介紹。

## 函數有三種被呼叫的時機

### 1. HTML 事件觸發

JavaScript 可以和 HTML 元件 (HTML Elements) 互動，例如點擊按鈕、偵測鍵盤、瀏覽器載入完畢等。

事件 (Events) 就是發生在 HTML 元件的狀況。

> W3Schools: HTML events are "things" that happen to HTML elements.

當事件被偵測到時，JavaScript 就能呼叫指定的函數執行對應動作。

HTML 事件有很多種類，最常見的就是 `onclick`：

```javascript
<button onclick="sayHello()">Click Me</button>
<script>
function sayHello(){
    alert('Hello');
}
</script>
```

### 2. 由其他 JavaScript 程式碼主動呼叫

這應該是程式中最普遍的呼叫方式：

```javascript
sayHello();

function sayHello(){
    console.log('Hello');
}
```

### 3. 立即函數 (Self-Invoking Functions)

#### 快速複習一下函數表達式 (Function Expressions) 的概念

Day19 介紹過函數表達式 (Function Expressions) 形式的函數定義語法：

* 匿名表達式 (Function Expressions w/o Function Name)
* 具名表達式 (Function Expressions w/ Function Name)

複習一個簡單的函數表達式範例：

```javascript
var sayHello = function () {
    console.log('Hello');
};

sayHello();     // "Hello"
```

語法可以分匿名和具名，但具名沒什麼意義。

```javascript
var sayHello = function abc() {
    console.log('Hello');
};

abc();          // ReferenceError: abc is not defined
```

因為**透過 Function Expressions 定義的函數，我們實際在使用時，無法透過函數名稱 (`abc`) 取得函數物件，而是透過變數名稱 (`sayHello`) 取得函數物件**，然後執行該函數物件。

#### 函數定義用小括號包起來，一律代表 Function Expressions

這個小地方容易有概念上的模糊。

以下是一個**宣告式 (Function Declarations)** 的函數用法：

```javascript
function sayHello() {
    console.log('Hello');
};

sayHello();     // "Hello"
```

`sayHello` 是函數名稱，**宣告式的函數，我們可以直接透過函數名稱來取得函數物件，並且執行**。

**但一旦使用小括號將函數宣告式包裝起來，原本的 Function Declarations 也會被視為 Function Expressions**。

例如以下：

```javascript
(function sayHello() {
    console.log('Hello');
});

sayHello();     // ReferenceError: sayHello is not defined
```

如前面提到，透過 Function Expressions 定義的函數，無法透過函數名稱 (`sayHello`) 取得函數物件本身。而上面例子並沒有將這個函數物件儲存於任何變數，等於沒有機會去呼叫它。

所以如果使用 Function Expressions，幾乎都會宣告一個變數去儲存，並且不為函數特別具名。

#### 對函數表達式作立即性的呼叫 (Self-Invoking Expressions)

但如果 Function Expressions 是一次性的，只會使用一次，而且是馬上使用，就可以不用將函數物件另外存放到變數裡，而是加上 `()` 符號要求立刻執行：

```javascript
(function () {
    console.log('Hello');
})();
```

執行結果：

```
Hello
```

這就是為什麼本篇文章將 Self-Invoking Expressions 稱為「立即函數」，精確的名稱應該要翻「自我呼叫的函數表達式」，但立即函數這個名稱我認為在概念上更有助記憶和理解。

如同前面提到，函數表達式可以具名，語法上可以被接受。但實際上具名的函數名稱無法被呼叫，毫無意義：

```javascript
(function abc() {
    console.log('Hello');
})();

abc();
```

執行結果：

```
Hello
Uncaught ReferenceError: abc is not defined
```

所以**立即函數不需要具名**。

#### 不能對函數宣告 (Function Declarations) 作立即呼叫

> W3Schools: You cannot self-invoke a function declaration.

例如以下語法是不成立的：

```javascript
function sayHello() {
    console.log('Hello');
}();
```

一定要先包裝成一個 Function Expression，才能進行立即呼叫。

## 總結

**函數有三種被呼叫的時機：** 1. HTML 事件觸發 2. 由其他 JavaScript 程式碼主動呼叫 3. 立即函數

**立即函數使用重點：**

* 函數定義用小括號 `(` 和 `)` 包起來，一律代表 Function Expressions (即使原本是 Function Declarations)。
* 在 Function Expressions 後面加上 `()`，就能作立即性的呼叫。
* 立即函數不需要具名。
* 不能對函數宣告 (Function Declarations) 作立即呼叫。

## References

* [W3Schools - JavaScript Functions](https://www.w3schools.com/js/js_functions.asp)
* [Javascript 開發學習心得 - 函數的多種寫法與應用限制](https://sweeteason.pixnet.net/blog/post/40371736)


# Day 25：不是多了塊魚 —— 立即函數的應用整理

上一篇介紹到立即函數 (Self-Invoking Functions) 的用法，過程有沒有產生一個疑惑？

例如大費周章包裝了一個立即函數：

```javascript
(function () {
    console.log('Hello');
})();
```

不如直接執行還比較乾脆：

```javascript
console.log('Hello');
```

一樣都是立刻執行的效果，何必脫褲子放屁，多此一舉，豈非多餘？

![](https://i.imgur.com/s7apwXU.png)\
(Source: [Youtube](https://www.youtube.com/watch?v=3iAGnItkHA0))

## 立即函數是脫褲子放屁？

程式是工具，工具是為了幫助解決問題而存在才有意義，不會單純因為語法酷炫或用起來很潮而增加價值。

一開始學到立即函數時，我的第一個感想確實是覺得很多餘。

但我相信每個語法的存在一定有其用意，因此試著去觀摩和整理使用立即函數的場合。

以下是我目前歸納出的用途，歡迎補充：

### 用途 1：封裝一次性的 Local Scope

可以封裝一段想立刻執行的程式碼，建立一次性的 Local Scope，讓一次性變數的生命週期留在函數內，避免汙染到 Global Scope。

#### Before:

在未經封裝前，一次性用途的變數 `temp` 暴露在全域作用域下，潛藏後續對其他程式造成干擾的風險：

```javascript
var temp = 10 + 5;
console.log("Answer is " + temp);

// 執行其他的任務
otherMission();
function otherMission(){
    console.log(temp);     // temp 是全域變數，仍然存在
}
```

執行結果：

```
Answer is 15
15
```

#### After:

透過立即函數作封裝，避免不必要的變數影響殘留：

```javascript
(function (){
    var temp = 10 + 5;
    console.log("Answer is " + temp);
})();

// 執行其他的任務
otherMission();
function otherMission(){
    console.log(temp);     // temp 不存在於此作用域
}
```

執行結果：

```
Answer is 15
Uncaught ReferenceError: temp is not defined
```

自然，也可以用普通的函數包裝，再透過函數呼叫來執行，差別是還要為函數作命名：

```
function execOneTime(){
    var temp = 10 + 5;
    console.log("Answer is " + temp);
}
execOneTime();

// 執行其他的任務
otherMission();
function otherMission(){
    console.log(temp);     // temp 不存在於此作用域
}
```

好的命名也是需要花腦細胞去想，既然只是一次性而且立即性的用途，那就用立即函數吧！

### 用途 2：為物件實字 (Object Literals) 的屬性初始值產生動態值

#### Before:

原本如果想為物件屬性產生動態數值，必須先產生基本的初始物件，再額外對屬性來動態賦值：

```javascript
var student = {};
student.score = Math.random();
console.log(student.score);    // 0.7779381225655557
```

#### After:

透過立即函數的用法，可以在物件實字初始化階段就動態產生結果：

```javascript
var student = {
    score: (function (){ return Math.random(); })()
};
console.log(student.score);    // 0.7779381225655557
```

### 用途 3：將字串內容轉成函數物件

資料傳遞格式 JSON 不支援儲存函數型態的資料，理論上無法用來傳遞函數物件。

但透過立即函數，配合 `eval()`，就有機會實現用 JSON 傳遞函數型態資料，例如以下範例：

```javascript
var text = '{ "name":"John", "age":"function () {return 30;}"}';
var person = JSON.parse(text);

console.log(person);            // {name: "John", age: "function () {return 30;}"}
console.log(typeof person.age); // "string"

var getAge = eval("(" + person.age + ")");
console.log(getAge);            // ƒ () {return 30;}
console.log(getAge());          // 30
```

> 如果不是特殊需求，實際專案不建議這樣的作法，會造成程式維護困難。

### 用途 4 : 閉包 (Closures) 的應用

透過立即函數的封裝實現閉包應用，例如：

```javascript
var add = (function () {
    var counter = 0;
    return function () {return counter += 1;}
})();

console.log(add);
console.log(add());
console.log(add());
console.log(add());
console.log(counter);
```

執行結果：

```
ƒ () {return counter += 1;}
1
2
3
Uncaught ReferenceError: counter is not defined
```

變數 `counter` 被宣告於 `add()` 的函數作用域裡 (Function Scope Level)，理論上離開函數就會被消滅。但透過閉包，可以讓 `counter` 繼續存活，但又不至於被 Global Context 直接操作，只能透過 `add()` 操控。

關於閉包，我們會另外詳細討論，此處就不贅述。

### 用途 5 : 瀏覽器書籤小工具

可以把 JavaScript 程式碼包裝成立即函數，存成瀏覽器的書籤，點下去就會立即執行。

例如常見的「解除右鍵」工具就是這種用法：

```javascript
javascript:(function() { function R(a){ona = "on"+a; if(window.addEventListener) window.addEventListener(a, function (e) { for(var n=e.originalTarget; n; n=n.parentNode) n[ona]=null; }, true); window[ona]=null; document[ona]=null; if(document.body) document.body[ona]=null; } R("contextmenu"); R("click"); R("mousedown"); R("mouseup"); R("selectstart");})()
```

## 總結

立即函數可以應用的用途：

* 封裝一次性的 Local Scope
* 物件實字 (Object Literals) 的屬性初始值產生動態值
* 將字串內容轉成函數物件
* 閉包 (Closures) 應用
* 瀏覽器書籤小工具

相信還有其他應用情境，歡迎補充！

## References

* [W3Schools - JavaScript Functions](https://www.w3schools.com/js/js_functions.asp)
* [Javascript 開發學習心得 - 函數的多種寫法與應用限制](https://sweeteason.pixnet.net/blog/post/40371736)


# Day 26：程式界的哈姆雷特 —— Pass by value, or Pass by reference？

![](https://i.imgur.com/3jXfT1v.png)\
(圖片素材來源: [網路圖片](https://cc2e7e3e75a47f276cc1-88ad9aa911bb90089a975b5bf54af6e6.r50.cf2.rackcdn.com/uploaded/l/0e7119966_1521467067_lego-hamlet.jpg))

「To be or not to be, that is the question.」

這是莎士比亞經典《哈姆雷特》中，哈姆雷特王子的名句，是一個人生真諦的千古大哉問。

你知道程式世界裡也有一個萬年經典題嗎？

![](https://i.imgur.com/pmU2gU1.png)\
(圖片素材來源: [網路圖片](https://i.ebayimg.com/images/i/400819020350-0-1/s-l1000.jpg))

「Pass by value, or pass by reference, that is the question.」

對程式開發者來說，一個程式語言的變數是 **Pass by value** 還是 **Pass by reference**，可是至關重要的問題。

變數內容的傳遞方式是 Pass by value (傳值) 或 Pass by reference (傳址)，對於程式開發是一個很基本、但極為重要的觀念，直接影響著程式對變數的操作，相信大部分開發者也都不陌生。

**那 JavaScript 是 Pass by value 還是 Pass by reference？為何有人說其實是 Pass by sharing？甚至有人說 JavaScript 只有 Pass by value？**

今天就來探討這個萬年引戰題。

## Pass by value vs. Pass by reference

在探討 JavaScript 是哪一者之前，照例先快速介紹一下問題本身：**什麼是 Pass by value 和 Pass by reference**？

### Pass by value (傳值)

以下是一個典型的 Pass by value 的例子：

```javascript
var x = 5;
var y = x;
console.log(x);     // 5
console.log(y);     // 5

x = 10;
console.log(x);     // 10
console.log(y);     // 5
```

* 變數 `x` 被賦予 `5` 這個數值，然後透過 `y = x` 的方式為變數 `y` 賦值，因此 `x` 和 `y` 一開始的變數內容都是 `5`。
* 接著針對 `x` 賦予新的值 `10`，此時 `x` 是 `10`，但 `y` 仍是 `5`。

為什麼改變 `x` 的值，`y` 會不受影響？

下面是一個概念性的示意圖：

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

**因為當變數內容傳遞方式是 Pass by value (傳值)，也就是將舊變數的值內容複製一份，放進一塊新的記憶體，讓新變數指向過去**。

以上面的例子來說，就是將變數 `y` 指向一塊新的記憶體，然後將變數 `x` 裡的值**複製**一份放進去，等於變數 `x` 和 `y` 裡存放的是兩份獨立的資料，其中一份改變，不會影響另一份。

### Pass by reference (傳址)

換來看一個Pass by reference的例子：

```javascript
var person1 = { money: 111 };
var person2 = person1;
console.log(person1);  // {money: 111}
console.log(person2);  // {money: 111}

person1.money = 222;
console.log(person1);  // {money: 222}
console.log(person2);  // {money: 222}
```

* 變數 `person1` 賦予一個物件，透過 `person2 = person1` 的方式為 `person2` 賦值，`person1` 和 `person2` 兩個物件的 `money` 屬性一樣都是 `111`。
* 針對 `person1.money` 更換值的內容，連 `person2` 的屬性也一起變動到。

下面是概念性的示意圖：

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

**因為當變數內容傳遞方式是 Pass by reference (傳址)，新變數會直接指向舊變數的記憶體位址，等於新舊變數共用同一個位址的資料**。

**所以 `person1` 和 `person2` 其實指向同一個物件**，就像同一個房間有兩個入口，從哪一個入口進去，看到的都是同一個房間內容。因此，對 `person1` 作任何變動，也等同對 `person2` 作變動。

## JavaScript 什麼時候是 Pass by value？什麼時候是 Pass by reference？

基本上**看變數的資料型別，決定傳遞行為是 Pass by value 或 Pass by reference**。

### 當變數的值是原生型別 (Primitive) 時，行為是 Pass by value

在 JavaScript 中，原生型別 (Primitive) 包含：

* String
* Number
* Boolean
* Undefined
* Null (註)

> 註：`null` 雖然用 `typeof` 運算的結果是回傳 `object`，但 `null` 在行為上歸屬原生型別。

以下是這五種型別的行為測試：

```javascript
/* string */
var str1 = "111";
var str2 = str1;
console.log(str1); // "111"
console.log(str2); // "111"

str1 = "222";
console.log(str1); // "222"
console.log(str2); // "111"


/* number */
var n1 = 111;
var n2 = n1;
console.log(n1); // 111
console.log(n2); // 111

n1 = 222;
console.log(n1); // 222
console.log(n2); // 111


/* boolean */
var bool1 = true;
var bool2 = bool1;
console.log(bool1); // true
console.log(bool2); // true

bool1 = false;
console.log(bool1); // false
console.log(bool2); // true


/* null */
var nu1 = null;
var nu2 = nu1;
console.log(nu1); // null
console.log(nu2); // null

nu1 = "something";
console.log(nu1); // "something"
console.log(nu2); // null


/* undefined */
var ud1 = undefined;
var ud2 = ud1;
console.log(ud1); // undefined
console.log(ud2); // undefined

ud1 = "something";
console.log(ud1); // "something"
console.log(ud2); // undefined
```

### 當變數的值是物件型別 (Object) 時，行為是 Pass by reference

在 JavaScript 中的物件型別常見的例如：

* Array
* Object

以下是這兩種型別的行為測試：

```javascript
/* array */
var ary1 = [1, 2, 3];
var ary2 = ary1;
console.log(ary1); // [1, 2, 3]
console.log(ary2); // [1, 2, 3]

ary1[0] = 99;
console.log(ary1); // [99, 2, 3]
console.log(ary2); // [99, 2, 3]


/* object */
var person1 = { money: 111 };
var person2 = person1;
console.log(person1);  // {money: 111}
console.log(person2);  // {money: 111}

person1.money = 222;
console.log(person1);  // {money: 222}
console.log(person2);  // {money: 222}
```

## Pass by sharing 又是怎麼一回事？

以上到目前為止，一切看起來很單純美好，沒什麼問題：JavaScript 裡，原生型別 (Primitive) 是 Pass by value，物件型別 (Object) 是 Pass by reference。

為何會冒出一個 Pass by sharing 的說法？

問題出在以下情境：

### 1. 函數參數傳遞

```javascript
function rename(obj){
    obj = { name: "XXX" };
}

var person = { name: "OneJar" };
console.log(person);
rename(person);
console.log(person);
```

上面這個範例，將物件 `person` 傳遞給函數 `rename()` 作為參數，並在 `rename()` 裡面改變值。

根據前面的結論，物件型別 (Object) 是 Pass by reference，那麼執行結果應該是先印 `{name: "OneJar"}` 再印 `{name: "XXX"}` 囉？

執行結果：

```
{name: "OneJar"}
{name: "OneJar"}
```

![](https://i.imgur.com/Pi6huxi.png)\
(Source: [網路圖片](https://img.appledaily.com.tw/images/twapple/640pix/20110919/_Other/826_altek01.jpg))

怎麼好像和前面說好的不一樣？

上述例子可以再作抽離，縮小範圍，只看對 `obj` 變更值的部分。例子中是使用**物件實字 (Object Literals)** 的方式來變更值。

### 2. 使用陣列實字 (Array Literals) 和物件實字 (Object Literals) 重新賦值

```javascript
/* array literals */
var ary1 = [1, 2, 3];
var ary2 = ary1;
console.log(ary1); // [1, 2, 3]
console.log(ary2); // [1, 2, 3]

ary1 = [99, 100];
console.log(ary1); // [99, 100]
console.log(ary2); // [1, 2, 3]


/* object literals */
var person1 = { money: 111 };
var person2 = person1;
console.log(person1);  // {money: 111}
console.log(person2);  // {money: 111}

person1 = { money: 222 };
console.log(person1);  // {money: 222}
console.log(person2);  // {money: 111}
```

可以發現到，雖然是物件型別 (Object) 的變數，**如果用實字 (Literals) 對變數作重新賦值，並不會連另一個變數一起變更**。

### 3. 使用第三方變數作重新賦值

即使不是直接用實字 (Literals)，而是透過第三方變數去作重新賦值，同樣不會影響到第二個變數：

```javascript
/* array re-assigned */
var ary1 = [1, 2, 3];
var ary2 = ary1;
console.log(ary1); // [1, 2, 3]
console.log(ary2); // [1, 2, 3]

var ary3 = [99, 100];
ary1 = ary3;
console.log(ary1); // [99, 100]
console.log(ary2); // [1, 2, 3]
console.log(ary3); // [99, 100]


/* object re-assigned */
var person1 = { money: 111 };
var person2 = person1;
console.log(person1);  // {money: 111}
console.log(person2);  // {money: 111}

var person3 = { money: 333 };
person1 = person3;
console.log(person1);  // {money: 333}
console.log(person2);  // {money: 111}
console.log(person3);  // {money: 333}
```

### Pass by reference 和 Pass by value 的融合版

從前面例子發現到，雖然是物件型別 (Object) 的變數，如果是對物件變數作重新賦值，只會變更自己的值，不會連另一個變數一起變更。

這和前面提到的 Pass by reference 行為似乎不太一樣，反而有點像 Pass by value。

如此一來，稱為 Pass by reference 也不對，稱為 Pass by value 也不對，於是就出現了 **Pass by sharing** 的說法。

![](https://i.imgur.com/Ek5XCoa.jpg)\
(Source: [網路圖片](https://www.google.com.tw/search?q=%E4%B8%83%E9%BE%8D%E7%8F%A0+%E8%9E%8D%E5%90%88%E8%A1%93\&rlz=1C1GCEU_zh-TWTW821\&source=lnms\&tbm=isch\&sa=X\&ved=0ahUKEwjU-rqW2cbeAhWDurwKHfIgDYwQ_AUIEygB\&biw=1745\&bih=853\&dpr=1.1#imgrc=3fvdxKl6L9GHlM:))

**不少人將 JavaScript 的變數內容傳遞方式，稱為 Pass by sharing**：

* 碰到原生型別 (Primitive)，表現行為是 Pass by value。
* 碰到物件型別 (Object)，如果只是對物件內容作操作(例如陣列元素或物件屬性)，表現行為是 Pass by reference。
* 碰到物件型別 (Object)，如果對物件作重新賦值，表現行為是 Pass by value。

或者也有人視為：**JavaScript 的 Primitive 是 Pass by Value，Object 是 Pass by sharing**。

## JavaScript 只有 Pass by value？

那為何有人說 JavaScript 只有 Pass by value？

這要稍微牽涉到變數的記憶體儲存方式。

### 我不是骨阿默，但我今天還是要來說一個電腦資料儲存原理的故事

電腦在執行程式的過程，是將資料暫存在記憶體裡，以供運算過程使用。

![](https://i.imgur.com/1n19mQ2.jpg)\
(Source: [網路圖片](https://www.adata.com/upload/products/list/226.jpg))

對電腦來說，記憶體每一個空間都有對應的位址，就像住址編號一樣。

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

想像一下，如果大家寫程式都得這樣：

```javascript
0x002 = 10;
0x085 = 5;
0x108 = 0x002 + 0x085;
console.log(0x108);
.........
.........
```

應該會有 87% 的程式開發者想要逃坑。

![](https://i.imgur.com/HFk2nx0.png)\
(Source: 進擊的巨人)

還好，一個偉大的發明拯救了程式苦海中的眾生：**變數**。

**宣告變數，就是跟記憶體要某一個位址的空間來存放資料**。

在高階程式語言裡一定有支援變數，讓開發者能用更具可讀性的名稱來儲存資料。

**對電腦來說，實際運作和辨認資料位置是以「位址」為準；變數名稱只是為了方便人類呼叫、類似別名的存在，由程式幫忙和實際的資料儲存位址作連結。**

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

而我們透過變數儲存資料時，由於電腦對記憶體儲存資料有其機制，**不同的資料型別會有不同儲存方式**。

以 JavaScript 來說會有兩種： 1. 資料是**原生型別 (Primitive) 時，變數在記憶體內儲存的是「資料的內容」**，也就是我們常稱的 Pass by value。 2. 資料是**物件型別 (Object) 時，變數在記憶體內儲存的是「資料的位址」**，通過這個位址，可以找到實際儲存資料的地方，也就是我們常稱的 Pass by reference

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

> 筆者按：這裡的介紹比較偏向概念性，大幅簡化細節，實際上作業系統底層對記憶體的操控機制沒那麼簡單。但對於上層高階語言來說，這樣概念性的理解應該足夠應付一般開發所需。

### 為什麼有人認為 JavaScript 只有 Pass by value？

我們先看原生型別 (Primitive) 的狀況：

```javascript
var x = 5;
var y = x;
console.log(x);     // 5
console.log(y);     // 5

x = 10;
console.log(x);     // 10
console.log(y);     // 5
```

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

再來看物件型別 (Object) 的狀況：

```javascript
var x = { money: 111 };
var y = person1;
console.log(x);  // {money: 111}
console.log(y);  // {money: 111}

x.money = 222;
console.log(x);  // {money: 222}
console.log(y);  // {money: 222}
```

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

從底層實際運作的角度來看，不管是原生型別 (Primitive) 或物件型別 (Object)，在作 `x = y` 這個動作時，都是把 `x` 內儲存的值複製一份到 `y` 裡 (不管這個值的內容是一個數值或一個位址)。

這就是為什麼有人認為 JavaScript 只有 Pass by value。

## 萬年引戰的癥結點：人人定義不同

Pass by value 或 Pass by reference 直接關係著程式的變數運作，而且是每個程式語言都會碰到，不是 JavaScript 獨有。

這種理當在程式語言開天闢地時代就遇到的基本議題，怎麼會到今日仍爭議不斷？

我覺得這篇文章 —— [深入探討 JavaScript 中的參數傳遞：call by value 還是 reference？](https://blog.techbridge.cc/2018/06/23/javascript-call-by-value-or-reference/) —— 點出一個癥結點：

> 大家對 call by reference 以及 call by value 的「定義」其實都不盡相同，而且也沒有一個權威性的出處能夠保證這個定義是正確的。

**例如所謂 Value，指的究竟是「資料的內容」，還是「存放在變數記憶體位址裡的值」？**

以下面變數 `n` 和 `obj` 來說：

```javascript
var n = 123;
var obj = { name: "OneJar" };
```

| 變數  | 資料的內容                | 存放在變數記憶體位址裡的值        |
| --- | -------------------- | -------------------- |
| n   | `123`                | `123`                |
| obj | `{ name: "OneJar" }` | 類似 `0x0001` 這樣的記憶體位址 |

* **如果 Value 指的是「資料的內容」**：
  * 物件 (Object) 間的 `obj1 = obj2` 傳值，就屬於 Reference 的複製，而非 Value 的複製。
  * 因此有 Pass by value 和 Pass by reference 分別。
* **如果 Value 指的是「存放在變數記憶體位址裡的值」**：
  * 所有的 `x = y` 傳值都屬於 Value 的複製。
  * 因此只有 Pass by value。

**這就是最初的定義不同、觀點不同，導致最後的結論不同**。

所以下面哪一句是對的： 1. JavaScript 只有 Pass by value。 2. JavaScript 的 Primitive 是 Pass by Value，Object 是 Pass by sharing。

答案都是對的，視乎立足的定義和觀點。

傳統 Java 在教變數資料型態時，Pass by Value 和 Pass by Reference 一直是一大考題。但如果從「Value 指的是**存放在變數記憶體位址裡的值**」的角度來看，這些考題全都該丟垃圾桶，因為都只有 Pass by Value。

其實不只是 Pass by Value 和 Pass by Reference 有這種名詞定義上的困擾，[各種 MV-Whatever 名詞定義](https://www.ithome.com.tw/voice/97137)也是爭吵不休。

會有這麼多技術名詞上的歧義，就像[這篇文章](https://www.ithome.com.tw/voice/94877)所言：

> 程式開發的世界中，名詞的創造經常是隨意的。
>
> 這些名詞通常沒有明確的定義，一開始只是為了溝通方便，而隨著聽聞者各自的領會與轉述，加上時間與歷史的推移，它們的定義會發生岐義而各自發展，最後失去了溝通的意義。

## 所以…… JavaScript 到底 Pass by WTF

[技術名詞紛爭多](https://www.ithome.com.tw/voice/94877)這篇文章最後總結得很好：

> 在《松本行弘的程式設計世界》的〈語彙與共通語言的重要性〉這篇文章中，作者談到，為某個概念決定適當的名詞，目的是在設計時能有共同的語彙，也能讓開發者意識到它們的存在，這才是名詞存在的真正意義。

**技術名詞是為了方便溝通，不是為了吵架**。

![](https://i.imgur.com/CTZSbHS.jpg)\
(Source: [網路圖片](https://0.share.photo.xuite.net/jingyueanfj/10e8827/8119399/320526272_m.jpg))

技術名詞的概念統一定義很重要，因為能讓大家在一個共通的語彙概念上溝通或學習。

這就是為什麼許多負責訂定標準或名詞定義的權威單位受人重視，他們所訂的名詞、定義等標準未必所有人都同意，但大部分的人願意接受和信服，因此大家一起遵循這樣的標準，使所有人能在同樣的理解共識下工作。

但如果為了在技術名詞上鑽牛角尖，反而把自己越弄越糊塗，變成為了捍衛技術名詞的存在而存在，我認為是本末倒置。

**技術名詞是為了描述概念而存在，而不是概念為了技術名詞而存在。**

**最重要的是背後期望表達的概念，也就是體現出來的「行為」。**

## References

* [深入探討 JavaScript 中的參數傳遞：call by value 還是 reference？](https://blog.techbridge.cc/2018/06/23/javascript-call-by-value-or-reference/)
* [技術名詞紛爭多](https://www.ithome.com.tw/voice/94877)
* [MV-Whatever大家族](https://www.ithome.com.tw/voice/97137)
* [\[筆記\] 談談JavaScript中by reference和by value的重要觀念](https://pjchender.blogspot.com/2016/03/javascriptby-referenceby-value.html)
* [重新認識 JavaScript: Day 05 JavaScript 是「傳值」或「傳址」？](https://ithelp.ithome.com.tw/articles/10191057)


# Day 27：別管變數 Pass by Whatever，尋找容易理解的銀色子彈 (Silver Bullet)

![](https://i.imgur.com/EmnBEyG.jpg)\
(Source: [網路圖片](https://www.nurtur-health.eu/WebRoot/StoreNL/Shops/62902058/50AB/33F7/232B/F188/F3CD/C0A8/28BE/BCA0/SBPacks_ml.JPG))

昨天的文章談到 Pass by value 和 Pass by reference。

一個程式語言的變數運作機制究竟是 Pass by value 還是 Pass by reference，不管在 JavaScript 或其他語言，一直都爭議不斷。

而爭議不斷的癥結點源自於名詞定義和觀點的不同。

**技術名詞是為了描述概念而存在，而不是概念為了技術名詞而存在**。

不需要對名詞字眼鑽牛角尖，**最重要的是背後的概念，也就是體現出來的「行為」**。

以行為結果來看，JavaScript 的變數行為大致可以歸納成以下：

* 碰到原生型別 (Primitive)，表現行為是 Pass by value。
* 碰到物件型別 (Object)，如果只是對物件內容作操作(例如陣列元素或物件屬性)，表現行為是 Pass by reference。
* 碰到物件型別 (Object)，如果對物件作重新賦值，表現行為是 Pass by value。

## 知道行為結果，但說不出為什麼

然而上述的行為整理對我而言，只是表面行為的描述，難以對背後原理提出一個解釋或理解模式。

例如同樣是物件型別 (Object)，為什麼重新賦值和非重新賦值的狀況，表現行為會不同？

也許深入鑽研到系統底層核心對記憶體的操縱機制，自然能提出完美解釋，但這對大多數上層應用程式開發者來說成本太高。

就算無法解釋為什麼，只要記熟上述行為結果，大概也足以應付程式中變數行為的推導。

但我對這種死記硬背規則的方式滿抗拒的。

尤其軟體開發要記的知識已經太多，身為一個金魚腦開發者，必須盡量用「理解」取代「硬記」，否則即使短時間內記得，也容易隨著時間變得印象模糊，更別提「硬記」這件事本身有多累。

![](https://i.imgur.com/j2x63SC.png)\
(Source: [網路圖片](Shttps://scontent-atl3-1.cdninstagram.com/vp/eb29f41249b13e02315e33b79c683a50/5C2829EA/t51.2885-15/e35/40268872_1509711539173717_664853700400906240_n.jpg))

**對於情境千變萬化的議題，我偏好試著去找出一個簡單、容易掌握、而且能夠自圓其說的模式，無論遇上任何情境都能用同一個模式推導出正確行為，就像一顆銀色子彈**。

> 銀色子彈（英語：Silver bullet）是一種由白銀製成的子彈，有時也被稱為銀彈。在西方的宗教信仰和傳說中，它作為一種武器，成為唯一能和狼人、女巫及其他怪物對抗的利器。
>
> 銀色子彈也可用於比喻，喻作強有力的，一勞永逸的，適應各種場合的解決方案。
>
> (引用自維基百科)

## 推導變數行為的銀色子彈：盒子圖像概念

我對銀色子彈的條件： 1. 化繁為簡，必須依靠記憶的前提規則越少越好 (否則叫金魚腦怎麼辦)。 2. 內容盡量質樸，越具體、越簡單、越貼近生活經驗，就越容易理解和運用，能圖像化更佳，有時候甚至帶點稚氣也無所謂 (有趣的童書和沉悶的論文，我更願意擁抱前者)。 3. 能適用所有情境，或至少必須適用絕大部分情境，僅需額外備註極少數的例外 (否則也不稱其為銀色子彈)。

**針對「推導各種程式碼情境的變數行為」，我的銀色子彈就是「盒子圖像概念」。**

其實這個思路我不認為是什麼創建，很多文章或教學講述的都是類似的概念。

**這篇文章比較像進行一個明確的歸納整理，套上盒子這種貼近生活的比喻，搭配圖像化的理解，期望讓這個概念成為一個具體且親切的模式，能被簡易且有系統地運用和分享。**

根據我的經驗，到目前為止碰到各種令人困惑混淆的程式碼情境，都可以用這套「盒子圖像概念」有個說得通的解釋，順利推導變數行為，幫助 Debug。

必須先聲明一件事，避免造成誤解：**這個「盒子圖像概念」純粹是對程式上層行為一個概念式的理解方法，用於上層變數行為的推導，不代表系統底層對記憶體位址的實際操作原理**。

對於深諳底層系統機制的高手來說，這個概念在許多地方的理解或描述可能是十分粗糙、遠遠不夠精確。

但以「推導變數行為」的目的而言，相信可以達到一定效果。

俗話說，黑貓白貓，能抓老鼠的就是好貓。

這顆銀色子彈也許不夠精美，最重要的是希望能幫助變數行為的程式碼撰寫更加容易。

## 盒子圖像概念的起手式

### 記憶體每一塊空間是一個盒子

記憶體有很多個儲存空間，就像一個個不同的盒子 (或抽屜的概念也行)。

* 盒子的名字 => 變數名稱
* 盒子編號是第幾號 => 記憶體位址
* 盒子裡面的東西 => 記憶體儲存的值

比如這樣一個儲存資料的變數：

```javascript
var age = 18;
```

用盒子的圖像概念來思考：

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

### 宣告變數就是跟電腦討一個盒子

例如下面是宣告變數的動作：

```javascript
var n;
var s;
var person;
```

已宣告變數但還沒賦值之前，盒子內的值就是 `undefined`。

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

### 使用實字 (Literals) 或 `new` 關鍵字，其實都是跟電腦討一個匿名盒子

實字 (Literals) 包含各種型別，例如數字實字、字串實字、陣列實字、物件實字等。

```javascript
5;
"Hello";
{name: “OneJar”, money: 250};
```

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

此外，`new` 這個關鍵字，就意涵著「跟電腦取得一塊新的記憶體」。

但光是討了盒子沒有用，我們不知道盒子的編號，無法將資料取出來用，**匿名盒子對開發者來說沒有用**。

在高階程式語言裡，記憶體位址幾乎不可控，通常由電腦分配。所以**需要把這些資料進一步傳到有名字的盒子裡，才能受開發者掌控**。

### 將資料放進有名字的盒子

```javascript
var n = 5;
var s = "Hello";
var person = {name: "OneJar", money: 250};
```

由於電腦系統底層對記憶體儲存資料的機制，根據資料的型別不同，盒子間傳遞值的方式有兩種狀況： 1. 原生型別 (Primitive)：

* 簡單型資料可以直接複製一份進變數盒子內。
* **這種類型我們稱為 Pass by Value**。
  1. 物件型別 (Object)：
* 複雜型資料無法直接複製進有名字的盒子內，所以只會把「資料的位址」放進變數盒子，讓程式自動根據位址去找到擁有實際資料的匿名盒子。
* **這種類型我們稱為 Pass by Reference**。

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

### 沒有用的匿名盒子很快會被消滅

俗話說限量是殘酷的，記憶體空間有限，為了能讓記憶體作最大化的運用，系統會回收已經用不到的盒子，以便提供給下一個人使用。

**用不到的盒子的標準是什麼？就是沒機會再被呼叫到**。

匿名盒子對開發者來說就是用不到的盒子，因為沒有辦法呼叫使用，在完成複製資料的任務之後，匿名盒子就會被系統回收走。

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

發現有一個位在 `0x093` 的匿名盒子倖存下來，為什麼？

因為這個匿名盒子和變數 `person` 之間有著引用關係 (Reference)，這個關係就像一條隱形的紅線，牽起兩個盒子之間親密的關係。

![](https://i.imgur.com/TeE6WGd.png)\
(Source: [網路圖片](https://www.google.com.tw/search?rlz=1C1SAVA_enTW523TW523\&biw=1280\&bih=882\&tbm=isch\&sa=1\&ei=OP7lW9nZNsT08QW346nwDA\&q=%E6%9F%AF%E5%8D%97+%E7%B4%85%E7%B7%9A\&oq=%E6%9F%AF%E5%8D%97+%E7%B4%85%E7%B7%9A\&gs_l=img.3...5065461.5068983.0.5069522.20.18.2.0.0.0.126.1122.14j3.17.0....0...1c.1j4.64.img..2.10.418...0j0i24k1.0.eiv3m89iKIc#imgrc=zSz_-sm0CaTzxM:))

透過變數 `person`，還有機會存取到 `0x093` 盒子的資料，因此 `0x093` 盒子還不會被回收。

換句話說，**只要身上還綁著紅線 (引用關係)，代表還被別人需要，盒子就不會被回收**。

### 引用關係 (Reference) 可以取消或改變

就像紅線可以被斷，引用關係可以被取消或移情別戀。

例如下面的例子：

```javascript
var person = {name: "OneJar", money: 250};
person = undefined;
person = {name: "John"};
```

不管是將 `person` 指派原生型別的資料，或是給予新的引用關係，都代表原本的引用失效，使得匿名盒子不再被任何人引用，進而被回收。

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

**只要還存在被別人需要的引用關係 (Reference)，匿名盒子就不會被回收**。

**反之，如果不存在被需要的引用關係 (Reference)，匿名盒子就視同被消滅**。

![](https://i.imgur.com/iiA5FDt.jpg)\
(Source: [網路圖片](https://pic.pimg.tw/locking1218/1377625199-1383853375.jpg))

## 盒子圖像概念新手村結束，準備上戰場

以上的概念都還滿單純，運用的幾乎都是多數開發者早已熟知的基礎概念。

但已經足以解釋為什麼 Pass by Reference 的情況下，函數參數有時候會變更到外部參數、有時候不會。

### (A) 會變更到外部參數的範例

範例程式碼如下：

```javascript
function rename(obj){
    obj.name = "XXX";
}

var person = { name: "OneJar" };
console.log(person);

rename(person);
console.log(person);
```

執行結果：

```
{name: "OneJar"}
{name: "XXX"}
```

我們跟著程式碼，用盒子圖像概念逐步來看。

#### 1. 宣告 `person` 並給予初始值

```javascript
var person = { name: "OneJar" };
console.log(person);
```

這時候還很單純，就是一個變數盒子 `person` 引用一個匿名盒子，匿名盒子裡存著實際資料 `{ name: "OneJar" }`。

如下圖示意：

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

#### 2. 呼叫函數 `rename()`，並傳入 `person` 作為參數

```javascript
rename(person);
```

這短短的一行程式其實作了很多事，這裡要用慢鏡頭分解。

#### 2.1. 建立一個函數執行環境 (Function Execution Context)

當呼叫某個函數執行時，JavaScript 首先會建立一個新的函數執行環境 (Function Execution Context)。

**函數的參數名稱也是一種變數宣告**，因此這裡會產生一個名為 `obj` 的變數盒子，值是 `undefined`。

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

#### 2.2. 承接傳入的參數

`obj` 的盒子建立後，就能去承接傳進來的參數 `person` 的值。`person` 的值是一個位址 `0x093`，所以 `obj` 內存的也是位址 `0x093`。

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

變數 `obj` 透過位址，等於和 `0x093` 的匿名盒子建立紅線，因此可以存取到實際資料 `{ name: "OneJar" }`。

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

#### 3. 執行函數內的操作物件

前置作業完成後，就可以正式來看函數內的執行動作：

```javascript
function rename(obj){
    obj.name = "XXX";
}
```

這裡要對變數 `obj` 的屬性 `name` 作操作。

由於透過 `obj`，存取的實際資料是位址 `0x093` 盒子，因此變數 `person` 印出來的資料也變成 `{name: "XXX"}`。

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

### (B) 不會變更到外部參數的範例

範例程式碼如下：

```javascript
function rename(obj){
    obj = { name: "XXX" };
}

var person = { name: "OneJar" };
console.log(person);

rename(person);
console.log(person);
```

執行結果：

```
{name: "OneJar"}
{name: "OneJar"}
```

同樣用盒子的概念來看這個例子。

前面部分是一樣的，我們可以稍微快轉到「2.2. 承接傳入的參數」之後：

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

#### 3. 執行函數內的操作物件

```javascript
function rename(obj){
    obj = { name: "XXX" };
}
```

這裡一樣用慢鏡頭來看。

#### 3.1. 為物件實字產生一個匿名盒子

先跟電腦要一個匿名盒子來暫時儲存 `{ name: "XXX" }` 這份資料：

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

#### 3.2. 將匿名盒子的資料傳給變數盒子 `obj`

由於匿名盒子裡裝的是物件型別的資料，無法直接複製一份進變數盒子，因此是存入一個位址的值。

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

可以看到，**其實從這一步開始，變數 `obj` 和 `person` 就已經沒有任何牽連**，因此最後 `person` 印出來仍是 `{name: "OneJar"}`。

## 總結

JavaScript 究竟是 Pass by value、Pass by reference、還是 Pass by sharing，我想在有一個權威性單位定出一個明確的定義觀點之前，恐怕很難有爭執完全結束的一天。

但比起在技術名詞上鑽牛角尖，我更傾向找到一個容易掌握的模式，增加對行為上的理解。

畢竟對現實的專案開發來說，比起訂立一個漂漂亮亮的技術名詞，確保對程式行為的掌握才是首要任務。

**在「盒子圖像概念」裡，Value 採用「資料的內容」的觀點角度，分為 Pass by value 和 Pass by reference 兩種行為**。

根據上面的逐步示範，Pass by reference 完全能夠解釋為什麼有時會影響到外部變數、有時不會，並不需要額外的 Pass by sharing 讓事情更複雜。

目前為止，我個人碰到各種令人混淆的程式碼情境，都可以套用「盒子圖像概念」來解釋和推導，幫助 Debug。

事實上這個概念我仍持續在嘗試補充，因為有些細節的解釋總覺得還不夠圓滑。

如果看倌發現有程式碼情境是這套模式無法解釋，歡迎分享，我很願意修正這套模式。

**就像技術名詞的目的是為了表達概念，銀色子彈是為了解決問題，重要的不是它們的存在本身，而是它們能不能達到目的**。

## References

* [深入探討 JavaScript 中的參數傳遞：call by value 還是 reference？](https://blog.techbridge.cc/2018/06/23/javascript-call-by-value-or-reference/)
* [技術名詞紛爭多](https://www.ithome.com.tw/voice/94877)
* [\[筆記\] 談談JavaScript中by reference和by value的重要觀念](https://pjchender.blogspot.com/2016/03/javascriptby-referenceby-value.html)
* [重新認識 JavaScript: Day 05 JavaScript 是「傳值」或「傳址」？](https://ithelp.ithome.com.tw/articles/10191057)
* [維基百科 - 銀色子彈](https://zh.wikipedia.org/wiki/%E9%8A%80%E8%89%B2%E5%AD%90%E5%BD%88)


# Day 28：閉包 (Closures)

閉包 (Closures) 是 JavaScript 中名號響噹噹的一個概念。鐵人賽接近尾聲，終於輪到閉包出場。

閉包是什麼呢？

我們來看一下 W3Schools 的定義：

> A closure is a function having access to the parent scope, even after the parent function has closed.

**閉包 (Closures) 是一個能存取父作用域的函數，即使父作用域已經結束**。

![](https://i.imgur.com/lcqSy7g.png)\
(Source: [網路圖片](https://colorfulblanche.com/wp-content/uploads/2018/01/%E6%8A%95%E5%BD%B1%E7%89%8713-1024x576.png))

看到這種論文式的定義說明就能心領神會的練武奇才，可以考慮左轉離開去找對你更有價值的知識 (誤)。

沒學過如來神掌的觀眾，我們還是從頭來好好認識一下閉包。

![](https://i.imgur.com/2ee8sjQ.jpg)\
(Source: [網路圖片](https://s9.rr.itc.cn/r/wapChange/20164_15_14/a0sisi1715111647352.JPEG))

## 閉包 (Closures) 先修課

閉包在 JavaScript 中是一個很重要、相對理解上也較為複雜的概念，運用很多其他 JavaScript 相關知識。

如果對這些必要的知識缺乏掌握，會造成閉包理解過程的阻礙。

因此正式踏入閉包副本打怪之前，建議先入手幾項技能包，否則越級打怪只會覺得痛苦，過程中似懂非懂、甚至不知所云。

好消息是，這些知識都是我們前面所介紹過的概念！

以下是傳送門整理：

1. **變數運作行為：**
   * [Day26：程式界的哈姆雷特 —— Pass by value, or Pass by reference？](https://ithelp.ithome.com.tw/articles/10209104)
   * [Day27：別管變數 Pass by Whatever，尋找容易理解的銀色子彈 (Silver Bullet)](https://ithelp.ithome.com.tw/articles/10209287)
2. **變數作用域 (Scope)：**
   * [Day5：湯姆克魯斯與唐家霸王槍——變數的作用域(Scope) (1)](https://ithelp.ithome.com.tw/articles/10203387)
   * [Day6：湯姆克魯斯與唐家霸王槍——變數的作用域(Scope) (2)](https://ithelp.ithome.com.tw/articles/10203306)
   * [Day7：傳統 var 關鍵字的不足](https://ithelp.ithome.com.tw/articles/10203548)
   * [Day8：var 掰掰 —— ES6 更嚴謹安全的 let 和 const](https://ithelp.ithome.com.tw/articles/10203715)
3. **Hoisting (宣告的提升效果)：**
   * [Day10：程式也懂電梯向上？ —— Hoisting](https://ithelp.ithome.com.tw/articles/10204951)
4. **立即函數：**
   * [Day24：函數呼叫 (Function Invocation) 與立即函數 (Self-Invoking Functions)](https://ithelp.ithome.com.tw/articles/10208709)

## 先不要管閉包，你有沒有聽過……

閉包是一個略為複雜的概念，特地發展出這樣一個複雜的運用，絕對不是為了自虐，一定是為了解決某個問題或情境。

咱們先不要管什麼是閉包，先來看個範例程式。

在前面的文章，我們已經知道作用域 (Scope) 的運作，根據作用域鏈 (Scope Chain)，Local Scope 可以存取到 Global Scope 的變數。

例如以下例子：

```javascript
var counter = 0;
function sellTicket(buyer){
  counter +=1;
  console.log(`(Total Sold: ${counter}) Buyer: ${buyer}`);
}

sellTicket('OneJar');          // (Total Sold: 1) Buyer: OneJar
sellTicket('Tony Stark');      // (Total Sold: 2) Buyer: Tony Stark
sellTicket('Steven Rogers');   // (Total Sold: 3) Buyer: Steven Rogers
```

每當成功賣出一張票，就會呼叫 `sellTicket()`，印出購票者，並將售票總數加一。

但這樣寫有一個缺點，就是**大家都能直接存取變數 `counter`，無法限制必須透過 `sellTicket()` 來保障變數 `counter` 的運作**，對於變數 `counter` 來說是不安全的。

例如可能發生這樣的事：

```javascript
var counter = 0;
function sellTicket(buyer){
  counter +=1;
  console.log(`(Total Sold: ${counter}) Buyer: ${buyer}`);
}

sellTicket('OneJar');          // (Total Sold: 1) Buyer: OneJar
sellTicket('Tony Stark');      // (Total Sold: 2) Buyer: Tony Stark
counter +=1;
sellTicket('Steven Rogers');   // (Total Sold: 4) Buyer: Steven Rogers
```

你可以說，那就口頭要求大家都必須透過 `sellTicket()` 來存取。

但你知我知獨眼龍也知，工程師哪有這麼乖。

![](https://i.imgur.com/UO4AaaY.png)\
(Source: [Youtube](https://www.youtube.com/watch?v=m_24DLLpNDU))

這道理就像小孩子跟爸媽拿零用錢，如果爸媽允許小孩直接去錢包自己拿，等於依賴小孩的自律，無法保證哪天小孩鬼迷心竅直接拿信用卡去刷爆。

> 筆者閒聊：類似的事件近年已經不是什麼[新聞](https://unwire.hk/2018/04/04/micro-transaction/fun-tech/)。

對程式來說，這種人為自律沒有保障，通常都不是惡意，但偶然不小心誤用就足以對程式系統造成麻煩。

所以程式設計中才會有封裝的概念，除了降低不必要的細節暴露，也提高安全控管，例如 getter、setter 就是典型例子。

那為了不讓大家都能直接存取 `counter`，我將變數 `counter` 放進 `sellTicket()` 裡，外部環境無法直接存取，一定要透過 `sellTicket()` 才能控制：

```javascript
function sellTicket(buyer){
  var counter = 0;
  counter +=1;
  console.log(`(Total Sold: ${counter}) Buyer: ${buyer}`);
}

sellTicket('OneJar');          // (Total Sold: 1) Buyer: OneJar
sellTicket('Tony Stark');      // (Total Sold: 1) Buyer: Tony Stark
sellTicket('Steven Rogers');   // (Total Sold: 1) Buyer: Steven Rogers
```

因為變數 `counter` 存在 Local Scope 裡，一旦每次 `sellTicket()` 的函數執行環境消失，變數 `counter` 就會跟著失效，導致總票數永遠都是 `1`，這顯然不合需求。

**由此可以歸納出我們需要的效果**： 1. 變數 `counter` 需要存在於的 Local Scope 裡，讓外部環境無法直接存取，必須透過 `sellTicket()` 來確保執行動作的安全。 2. 但即使函數執行結束，變數 `counter` 還是能持續存活不失效。

這就是閉包想要做到的事情。

## 從目標分析閉包需要的要素

上述目標效果主要有兩個要點：

### 1. 變數資料存在於的 Local Scope 裡，讓外部環境無法直接存取，以確保動作安全

換句話說，將我們希望操控的變數宣告於一個 Local Scope 內，限制它的存取權。

這是相對容易做到的，一個普通的函數就能達到目的。

**所以我們知道了閉包的第一個要素：函數。**

比較麻煩的是第二點。

### 2. 即使 Local 的執行環境結束，Local 環境內建立的資料還是能持續存活

每一次呼叫函數，都是建立一個新的函數執行環境，一旦函數執行結束，這個環境就會失效，包含環境內所宣告的變數，這也是 Local Scope 能成立的前提。

那在這個前提下，**如何做到即使函數執行環境結束，裡面的變數能持續存活？**

還記得 [Day27](https://ithelp.ithome.com.tw/articles/10209287) 我們提到使用盒子圖像的概念來思考資料儲存。

在一個函數裡宣告一個變數，就是產生一個新的變數盒子。

而當函數執行結束，函數執行環境失效，等於變數的宣告失效，就代表拿掉盒子的變數名稱，變成一個匿名盒子。

匿名盒子因為無法再被呼叫使用，很快就會被系統回收，徹底消滅。

**那什麼情況下，一個匿名盒子可以繼續存活不被回收？**

> 只要盒子身上還綁著被別人需要的紅線，盒子就不會被回收。

**紅線就是引用關係 (Reference)。**

舉例來說以下的例子：

```javascript
function createCar(brandName){
  var car = { brand:brandName };
  return car;
}

var myCar = createCar("BMW");
console.log(myCar);   // {brand: "BMW"}
```

我們用盒子圖像概念來分解過程發生什麼事。

**1. 呼叫 `createCar()`，建立一個新的函數執行環境，也就是一個 Local 環境。**

```javascript
function createCar(brandName){
  ......
}

var myCar = createCar("BMW");
.....
```

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

* Global 環境呼叫 `createCar()` 的同時，產生一個字串實字 (Object Literals) `"BMW"`，暫時放在位址 `0x001` 的盒子，準備用來傳入參數。
* Local 環境自動宣告一個 Local 變數 `brandName` 負責儲存參數。
* 將 Global 環境的 `"BMW"` 資料複製到 Local 變數 `brandName`。
* (完成複製資料的任務後，位址 `0x001` 的匿名盒子因為不再有機會被呼叫，很快就會被回收。)

**2. 在 Local 環境內產生一個新物件。**

```javascript
function createCar(brandName){
  var car = { brand:brandName };
  .....
}
```

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

* 利用變數 `brandName` 的資料當素材，產生一個物件實字 (Object Literals) `{brand: "BMW"}`，放在位址 `0x003` 的匿名盒子。
* 宣告一個 Local 變數 `car` 負責儲存新物件，但因為新物件是物件型態 (Object Types)，無法直接存放到 `car` 變數盒子裡，因此儲存的是位址 `0x003`，建立引用關係。

**3. 將 Local 環境的物件回傳給 Global 環境。**

```javascript
function createCar(brandName){
  var car = { brand:brandName };
  return car;
}

var myCar = createCar("BMW");
.......
```

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

* Global 環境宣告變數 `myCar` 準備承接回傳資料。
* Local 環境回傳 `car` 變數，將 `car` 變數的內容傳給 `myCar`，也就是 `0x003` 這個位址。
* `myCar` 和存放 `{brand: "BMW"}` 的匿名盒子建立引用關係。

**4. Local 環境結束。**

```javascript
.......
console.log(myCar);   // {brand: "BMW"}
```

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

* 在 Local 環境宣告的變數名稱失效，沒有機會再被呼叫的盒子都被消滅回收。
* 但位址 `0x003` 的匿名盒子因為還存在引用關係，所以仍然存活。

這就是為什麼理論上 `{brand: "BMW"}` 是 Local Scope 產生的資料，在 Global 環境卻可以繼續使用。

**這就是閉包的第二個要素：引用。**

## 正式進入閉包的用法

前面了解到，**函數和引用是閉包的原理重點**。

我們來看要如何實際應用。

### 閉包基本示範

我們延續前面的 sellTicket 範例，來看看如何用閉包達到我們的目的。

以下是這個範例的閉包寫法：

```javascript
function getSellTicketClosure(){
  var counter = 0;
  function action(buyer){
    counter +=1;
    console.log(`(Total Sold: ${counter}) Buyer: ${buyer}`);
  }
  return action;
}

var sellTicket = getSellTicketClosure();

sellTicket('OneJar');          // (Total Sold: 1) Buyer: OneJar
sellTicket('Tony Stark');      // (Total Sold: 2) Buyer: Tony Stark
sellTicket('Steven Rogers');   // (Total Sold: 3) Buyer: Steven Rogers
```

解說：

* 利用 `getSellTicketClosure()` 的包裝，讓變數 `counter` 建立在 Local 環境，無法被 Global 環境直接存取。
* 將執行動作的邏輯仍然打包成一個函數，名為 `action()`，但是宣告在 `getSellTicketClosure()` 內，也就是一個內部函數。
* 將 `action()` 的**函數物件**回傳出去給 Global 環境，宣告一個 `sellTicket` 負責承接。
* **`sellTicket` 是一個函數物件，也是一個閉包**。

可以發現這裡還運用了函數作用域的一個性質：**內部函數可以使用父函數的變數**。

也就是內部函數 `action()` 可以使用父函數 `getSellTicketClosure()` 裡的變數 `counter`。

**這個性質是將閉包的兩個要素——函數和引用——串聯起來的重要關鍵。**

這裡用圖像來進一步詳解過程：

**1. 執行 `getSellTicketClosure()`，建立一個 Local 環境，宣告 Local 變數和內部函數。**

```javascript
function getSellTicketClosure(){
  var counter = 0;
  function action(buyer){
    counter +=1;
    console.log(`(Total Sold: ${counter}) Buyer: ${buyer}`);
  }
  ..............
}

var sellTicket = getSellTicketClosure();
.......
```

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

* 在 Local Scope 宣告變數 `counter`，初始值是 `0`。
* 宣告一個函數 `action`，包裝要執行的動作邏輯。在 JavaScript 內函數是一種物件，因此相當於宣告一個變數名叫 `action`，儲存函數物件的位址。
* 函數 `action` 裡的動作使用到 `counter` 變數，也就是說**內部函數引用了父函數的變數**。

**2. 將內部函數作為物件回傳出去 Global 環境。**

```javascript
function getSellTicketClosure(){
  .........
  return action;
}

var sellTicket = getSellTicketClosure();
...........
```

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

* 將函數物件 `action` 回傳出去。
* Global 環境宣告一個變數 `sellTicket` 負責承接傳出來的 `action`，也就是函數物件的位址 `0x002`。

**3. `getSellTicketClosure()` 的 Local 環境結束。**

```javascript
...........

var sellTicket = getSellTicketClosure();

sellTicket('OneJar');          // (Total Sold: 1) Buyer: OneJar
sellTicket('Tony Stark');      // (Total Sold: 2) Buyer: Tony Stark
sellTicket('Steven Rogers');   // (Total Sold: 3) Buyer: Steven Rogers
```

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

* Local 執行環境結束，宣告的 Local 變數名稱失效，原本的 Local 變數盒子都變成匿名盒子。
* 沒有機會再被呼叫，也沒有被別人引用的匿名盒子，都會被回收。
* 位址 `0x002` 的盒子，被 `sellTicket` 引用，所以持續存活。
* 位址 `0x001` 的盒子，被位址 `0x002` 引用，所以間接活著。
* Global 環境無法直接存取位址 `0x001` 的盒子，必須透過 `sellTicket`，去引用位址 `0x002` 的函數，保障了原本 `counter` 變數的資料安全。

### 每一個閉包保存的都是一個獨立的環境

例如以下程式碼：

```javascript
function getSellTicketClosure(){
  var counter = 0;
  function action(buyer){
    counter +=1;
    console.log(`(Total Sold: ${counter}) Buyer: ${buyer}`);
  }
  return action;
}

var sellTicket1 = getSellTicketClosure();
sellTicket1('OneJar');            // (Total Sold: 1) Buyer: OneJar
sellTicket1('Tony Stark');        // (Total Sold: 2) Buyer: Tony Stark
sellTicket1('Steven Rogers');     // (Total Sold: 3) Buyer: Steven Rogers

var sellTicket2 = getSellTicketClosure();
sellTicket2('Luffy');             // (Total Sold: 1) Buyer: Luffy
sellTicket2('Nami');              // (Total Sold: 2) Buyer: Nami
```

由於每一次呼叫 `getSellTicketClosure()`，都是建立一個新的函數執行環境，所以回傳的閉包函數也都各自獨立不互相干擾。

### 利用立即函數簡化語法

由於函數 `getSellTicketClosure()` 的目的只是為了回傳一個閉包的函數物件，定義後可以馬上執行，而且只會執行一次，因此語法上可以用立即函數作簡化。

此外，裡面的內部函數只是用於定義動作，不需要在父函數裡執行，因此函數名稱也不重要。

簡化後的語法如下：

```javascript
var sellTicket = (function(){
  var counter = 0;
  return function(buyer){
    counter +=1;
    console.log(`(Total Sold: ${counter}) Buyer: ${buyer}`);
  }
})();

sellTicket('OneJar');            // (Total Sold: 1) Buyer: OneJar
sellTicket('Tony Stark');        // (Total Sold: 2) Buyer: Tony Stark
sellTicket('Steven Rogers');     // (Total Sold: 3) Buyer: Steven Rogers
```

## 總結

目前看到的閉包教學，大部分都是從「定義」或「語法」切入，然後試著在過程中體會閉包的「目的」。

但是這種方式我覺得相對抽象，**比較像為了學閉包而學，而不是為了解決問題**，一開始理解上容易有隔閡，需要花比較長的時間慢慢體會閉包的效果。

因此本篇文章嘗試**從「目的」切入，先提出遇到的困境、分析解決困境需要什麼要素，然後自然導出閉包的用法，比較屬於目標導向的思路**。

希望這樣的思路能提供不一樣的風格，讓學習閉包之路更為順暢。

現在再來看文章開頭的閉包定義，應該就比較有感：

> A closure is a function having access to the parent scope, even after the parent function has closed.

**閉包 (Closures) 是一個能存取父作用域的函數，即使父作用域已經結束**。

**閉包重點整理：**

* 閉包是一個函數，能「記得」被建立時的環境的一種機制。
* 閉包運用的技巧：
  1. 函數對變數的 Local Scope 封裝。
  2. 內部函數對外層函數變數的引用。
  3. 回傳內部函數的物件，形成閉包。
* 閉包實際上儲存的是對外層函數變數的引用 (References)。
* 每一個閉包中保存的都是一個獨立的環境，不同閉包間不互相干擾。
* 可以用立即函數的寫法來簡化語法。

## References

* [W3Schools - JavaScript Closures](https://www.w3schools.com/js/js_function_closures.asp)
* [JavaScript Function Closure (閉包)](https://www.fooish.com/javascript/function-closure.html)
* [Javascript中的傳遞參考與closure (2)](https://ithelp.ithome.com.tw/articles/10130860)


# Day 29：閉包 (Closures) 進階打怪實戰

昨天介紹了基本的閉包用法，本篇就來看一些比較進階的閉包應用，或是情境比較複雜的例子。

## 模擬 Class 物件導向用法中的私有成員變數效果

用過其他 Class-based 物件導向語言的開發者，對於 `private` 用法一定不陌生。

例如以下是一個 Java 的私有成員變數範例：

```java
class Person{
    private String name;
    public Person(String n) { this.name = n; }
    public String getName() { return this.name; }
    public void setName(String n) { this.name = n; }
    public String sayHi() { return "Hi I am " + this.name; }
}

public class HelloWorld{
    public static void main(String []args){
        Person p1 = new Person("OneJar");
        Person p2 = new Person("Tony Stark");
        System.out.println(p1.sayHi());
        System.out.println(p2.sayHi());

        p2.setName("Steven Rogers");
        System.out.println(p2.sayHi());
    }
}
```

執行結果：

```
Hi I am OneJar
Hi I am Tony Stark
Hi I am Steven Rogers
```

類別 `Person` 的成員變數 `name` 被設為私有 (private) 屬性，類別外的使用者無法直接對 `name` 作存取，必須透過開放的函式，例如 `getName()`、`setName()`、`sayHi()`。

這樣的好處是可以確保 `name` 的資料安全性，能對 `name` 作什麼程度的資料操控，取決於 `Person` 類別願意開放多少動作函式。

JavaScript 並非傳統 Class-based 物件導向語言，缺乏這種語法支援。

雖然可以用 `new` 關鍵字搭配函數建構子 (Function Constructors) 的用法，模擬類似物件導向效果，但仍缺乏私有成員變數的效果，無法限制外部直接對成員變數作存取。

例如下面這個範例：

```javascript
function Person(n) {
    this.name = n;
    this.sayHi = function(){
        return "Hi, I'm " + this.name;
    }
}

var p1 = new Person("OneJar");
var p2 = new Person("Tony Stark");
console.log(p1.sayHi());    // "Hi, I'm OneJar"
console.log(p2.sayHi());    // "Hi, I'm Tony Stark"

p2.name = "Steven Rogers";  // (外部仍可以直接存取)
console.log(p2.sayHi());    // "Hi, I'm Steven Rogers"
```

ES6 提供了 Class 的語法，但只是一種語法糖，讓語法看起來和傳統 Class-based 寫法相像，本質仍是傳統 JavaScript，而非傳統 Class-based 物件導向。

例如下面是 ES6 的 Class 寫法，仍舊可以直接對 `name` 屬性作存取：

```javascript
class Person {
    constructor(name) {
        this.name = name;
    }
    sayHi(){
        return `Hi I am ${this.name}`;
    }
}

var p1 = new Person("OneJar");
var p2 = new Person("Tony Stark");
console.log(p1.sayHi());    // "Hi, I'm OneJar"
console.log(p2.sayHi());    // "Hi, I'm Tony Stark"

p2.name = "Steven Rogers";  // (外部仍可以直接存取)
console.log(p2.sayHi());    // "Hi, I'm Steven Rogers"
```

但是如果透過閉包，就能模擬出類似私有成員的效果：

```javascript
function createPerson(name){
    var methods = {
      getName: function() { return name; },
      setName: function(n) { name = n; },
      sayHi: function() { return `Hi I am ${name}`; }
    }
    return methods;
}

var p1 = createPerson('OneJar');
var p2 = createPerson('Tony Stark');

console.log(p1.sayHi());           // "Hi I am OneJar"
console.log(p2.sayHi());           // "Hi I am Tony Stark"

p2.setName('Steven Rogers');
console.log(p2.sayHi());           // "Hi I am Steven Rogers"
```

* 變數 `name` 可以持續存活於閉包環境，不會因函數結束而失效。
* 可以對 `name` 作什麼程度的操控，取決於定義時願意開放多少動作函數。例如想修改 `name`，就一定要透過 `setName()`。
* 每個閉包引用的都是獨立的環境，因此 `p1` 和 `p2` 不互相干擾。

## 閉包引用外層函數變數的混淆範例

下面是偶然看到的閉包範例，覺得非常有趣。

### 範例 1

```javascript
function buildFunctions() {
  var arr = [];

  for(var i = 0; i < 3; i++) {
    arr.push(function() {
      console.log(i);
    });
  }

  return arr;
}

var fs = buildFunctions();
fs[0]();
fs[1]();
fs[2]();
```

`fs` 是一個陣列，儲存了 3 個函數，各別呼叫會印出什麼？

我第一眼看過去，直覺以為會印出 `0`、`1`、`2`，因為不是說每個閉包都是獨立的環境嗎？

執行結果：

```
3
3
3
```

![](https://i.imgur.com/JC52Dvn.png)\
(Source: [白爛貓貼圖](https://store.line.me/stickershop/product/1236945/?ref=Desktop))

趕快出動我的銀色子彈，分析一下發生什麼事。

**1. 呼叫 `buildFunctions()` ，宣告陣列變數：**

```javascript
function buildFunctions() {
  var arr = [];
  ...........
}

var fs = buildFunctions();
..........
```

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

* 陣列在 JavaScript 裡是 Object 型別，所以 `arr` 變數盒子裡存放的會是一個位址，引用到陣列實際的資料位置。

**2. 進入 for 迴圈，當 for 迴圈的 `i = 0`：**

```javascript
function buildFunctions() {
  var arr = [];
  for(var i = 0; i < 3; i++) {
    arr.push(function() {
      console.log(i);
    });
  }
  .................
}

................
```

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

* 宣告變數 `i` 作為計數器。
* 建立一個新的函數物件，存放在匿名盒子 `0x101`。
* `0x101` 函數物件內容的 `console.log(i)`，會引用變數 `i`。
* `arr[0]` 會引用 `0x101`。

**3. 當 for 迴圈的 `i = 1`：**

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

* 建立一個新的函數物件，存放在匿名盒子 `0x102`。
* `0x102` 函數物件內容的 `console.log(i)`，會引用變數 `i`。
* `arr[1]` 會引用 `0x102`。

**4. 當 for 迴圈的 `i = 2`：**

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

* 建立一個新的函數物件，存放在匿名盒子 `0x103`。
* `0x103` 函數物件內容的 `console.log(i)`，會引用變數 `i`。
* `arr[2]` 會引用 `0x103`。

**5. 當 for 迴圈的 `i = 3`，離開迴圈：**

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

* 變數 `i` 的值會再被加一，所以 `i` 的內容是 `3`，然後才跳離迴圈。

**6. 回傳 `arr` 到 Global 環境：**

```javascript
function buildFunctions() {
  ............
  return arr;
}

var fs = buildFunctions();
.............
```

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

* Global 環境宣告變數 `fs`，承接 `arr` 所存的位址 `0x002`。
* 變數 `fs` 和 `0x002` 建立引用關係。

**7. Local 環境結束，回收用不到的盒子：**

```javascript
................

var fs = buildFunctions();
fs[0]();
fs[1]();
fs[2]();
```

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

* `0x001` 的變數名稱 `arr` 失效，變成匿名盒子，且沒有被任何人引用，回收。
* `0x003` 的變數名稱 `i` 失效，變成匿名盒子，但因為還被其他人引用，因此繼續存活。
* `0x002` 原本就是匿名盒子，原本的引用者 `arr` 被回收，但仍被 Global 變數 `fs` 引用，因此繼續存活。
* `fs[0]`、`fs[1]`、`fs[2]` 裡面的 `console.log(i)` 都是引用 `0x003`，因此印出來都是 `3`。

### 範例 2

延續上面的範例，如果希望印出來是 `0`、`1`、`2` 呢？

可以利用 `let` 宣告一個 Block Scope 的變數來達到效果：

```javascript
function buildFunctions() {
  var arr = [];

  for(var i = 0; i < 3; i++) {
    let j = i;             // 用 `let` 宣告變數 `j`
    arr.push(function() {
      console.log(j);      // 這裡引用變數 `j`
    });
  }

  return arr;
}

var fs = buildFunctions();
fs[0]();
fs[1]();
fs[2]();
```

執行結果：

```
0
1
2
```

為什麼這樣就能印出 `0`、`1`、`2`？

**1. 呼叫 `buildFunctions()`，宣告陣列變數：**

```javascript
function buildFunctions() {
  var arr = [];
  ...........
}

var fs = buildFunctions();
..........
```

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

* 這裡和範例 1 沒有差別。

**2. 進入 for 迴圈，當 for 迴圈的 `i = 0`，建立新的 Block Scope：**

```javascript
function buildFunctions() {
  ..................
  for(var i = 0; i < 3; i++) {
    let j = i;
    ...................
  }
  .......................
}

................
```

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

* 用 `var` 宣告變數 `i` 作為計數器，`i` 屬於 Function Scope。
* 用 `let` 宣告變數 `j`，**因為用 `let` 宣告，會產生一個新的 Block Scope**。
* `j` 複製一份當前 `i` 的值。

**3. 當 for 迴圈的 `i = 0`，產生新的函數物件：**

```javascript
function buildFunctions() {
  .....................
  for(var i = 0; i < 3; i++) {
    let j = i;
    arr.push(function() {
      console.log(j);
    });
  }
  ................
}

.............
```

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

* 建立一個新的函數物件，存放在匿名盒子 `0x101`。
* `0x101` 函數物件內容的 `console.log(j)`，會引用變數 `j`。
* `arr[0]` 會引用 `0x101`。

**4. 當 for 迴圈的 `i = 0` 結束，Block Scope 失效：**

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

* `0x004` 的變數名稱 `j` 失效，變成匿名盒子，但因為還被其他人引用，因此繼續存活。

**5. 當 for 迴圈的 `i = 1`，建立新的 Block Scope：**

```javascript
function buildFunctions() {
  ..................
  for(var i = 0; i < 3; i++) {
    let j = i;
    ...................
  }
  .......................
}

................
```

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

* 再次用 `let` 在新的 Block Scope 宣告變數 `j`，儲存當前 `i` 的值。
* 和 `i = 0` 時是不同的 Block Scope，所以 `j` 的變數資料互相無關。

**6. 當 for 迴圈的 `i = 1`，產生新的函數物件：**

```javascript
function buildFunctions() {
  .....................
  for(var i = 0; i < 3; i++) {
    let j = i;
    arr.push(function() {
      console.log(j);
    });
  }
  ................
}

.............
```

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

* 建立一個新的函數物件，存放在匿名盒子 `0x102`。
* `0x102` 函數物件內容的 `console.log(j)`，會引用變數 `j`。
* `arr[1]` 會引用 `0x102`。

**7. 當 for 迴圈的 `i = 1` 結束，Block Scope 失效：**

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

* `0x005` 的變數名稱 `j` 失效，變成匿名盒子，但因為還被其他人引用，因此繼續存活。

**8. 當 for 迴圈的 `i = 3`，離開迴圈：**

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

* 當 `i = 2` 的情況依此類推。
* 當 `i = 3` 時，然後跳離迴圈。

**9. 將 `arr` 回傳到 Global 環境：**

```javascript
function buildFunctions() {
  ............
  return arr;
}

var fs = buildFunctions();
.............
```

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

* Global 環境宣告變數 `fs`，承接 `arr` 所存的位址 `0x002`。

**10. Local 環境結束，回收用不到的盒子：**

```javascript
................

var fs = buildFunctions();
fs[0]();
fs[1]();
fs[2]();
```

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

* `0x001` 的變數名稱 `arr` 失效，變成匿名盒子，且沒有被任何人引用，回收。
* `0x003` 的變數名稱 `i` 失效，變成匿名盒子，且沒有被任何人引用，回收。
* `0x002` 是匿名盒子，被 Global 變數 `fs` 引用，繼續存活。
* `fs[0]`、`fs[1]`、`fs[2]` 裡面的 `console.log(j)` 各自引用 `0x004`、`0x005`、`0x006`，因此印出結果是 `0`、`1`、`2`。

### 範例 3

如果範例 2 不是用 `let` 宣告變數 `j`，改用 `var`，其他程式碼都不變：

```javascript
function buildFunctions() {
  var arr = [];

  for(var i = 0; i < 3; i++) {
    var j = i;              // 改用 `var` 宣告
    arr.push(function() {
      console.log(j);
    });
  }

  return arr;
}

fs2 = buildFunctions();

fs2[0]();
fs2[1]();
fs2[2]();
```

會印出 `0`、`1`、`2` 還是 `3`、`3`、`3` 呢？

執行結果：

```
2
2
2
```

![](https://i.imgur.com/ap8KLrg.png)\
(Source: [網路圖片](https://fs2.my-bras.com/upload/ftp/00.Web/04.Column/Chau.jpg))

為什麼？

**這是因為變數宣告的 Hoisting 效果**，會將變數宣告提到 Scope 最頂端，**而用 `var` 宣告的變數屬於 Function Scope Level**，所以變數 `i` 和 `j` 都相當於宣告在函數一開始：

```javascript
function buildFunctions() {
  var arr = [];
  var i, j;    // 因為 Hoisting 效果，相當於宣告在這
  for(i = 0; i < 3; i++) {
    j = i;
    arr.push(function() {
      console.log(j);
    });
  }

  return arr;
}
```

所以範例 3 和 範例 1 的狀況是相近的，在函數 `buildFunctions()` 內只會產生一次 `j` 的變數盒子，三個閉包函數都是引用同一個 `j` 的資料盒子，所以印出來的結果都一樣。

而之所以印出 `2` 而非 `3`，是 for 迴圈的關係，`j = i` 在 for 迴圈內執行，但當 `i = 3` 時並不會進入迴圈內，因此 `j` 會停留在 `2`。

這個範例原理和範例 1 類似，就不附分解示意圖 (用手畫可能不用 1 分鐘，畫成投影片超乎想像地費時……Orz)。

## 總結

變數的引用 (References) 相對抽象，需要自己想像變數間的牽連。

閉包概念就是建立在函數和引用的基礎上，而程式碼可能組成的情境又是千變萬化，一個疏忽可能就想錯了結果。

為了確保自己不會想錯程式邏輯，找到一個套路，類似數學公式的效果，幫助開發過程不管遇到什麼程式碼情境，都能套用同一套思考模式來導出正確的行為，也就是我暱稱的銀色子彈。

從這篇文章的範例來看，銀色子彈的效果還不錯，可以避免自己一些似是而非的直覺性邏輯。

## References

* [W3Schools - JavaScript Closures](https://www.w3schools.com/js/js_function_closures.asp)
* [JavaScript Function Closure (閉包)](https://www.fooish.com/javascript/function-closure.html)
* [Javascript中的傳遞參考與closure (2)](https://ithelp.ithome.com.tw/articles/10130860)
* [Day 20 閉包](https://ithelp.ithome.com.tw/articles/10207900)


# Day 30：ES10 醞釀中 —— 擁抱 JS の 未來

終於來到最後一天尾聲。

![](https://i.imgur.com/e77c9Yp.png)\
(Source: [白爛貓貼圖](https://store.line.me/stickershop/product/1236945/?ref=Desktop))

## 擁抱 JavaScript 的未來

這篇文章的題目是擁抱 JavaScript 的未來，就讓我們在尾聲來看一下 JavaScript 的歷史和未來。

### JavaScript 的誕生

先簡單介紹一下 JavaScript 的背景。還記得 Day 1 文章看到的這位老兄嗎？

![](https://i.imgur.com/mbI00JX.png)\
(Source: [Thanks Brendan for giving us the Javascript : ProgrammerHumor - Reddit](https://www.reddit.com/r/ProgrammerHumor/comments/8srix1/thanks_brendan_for_giving_us_the_javascript/))

這位笑得你心裡發寒的，就是 JavaScript 的發明者 Brendan Eich。

1995 年 Brendan Eich 在 Netscape 公司的網羅下，為用戶端瀏覽器發明一個腳本語言，最初命名為 Mocha，後來改名 LiveScript，隨後為了搭上當時 Java 話題性的順風車，再度易名為 JavaScript。

(Wiki 上說 Brendan 只用 10 天就設計出原型，果真神人……)

這個商業上的命名考量也造成歷來許多人的誤解和疑惑：Java 和 JavaScript 有什麼關係？

![](https://i.imgur.com/6qUTmr7.png)\
(Source: [Wiki - 網景](https://zh.wikipedia.org/wiki/%E7%B6%B2%E6%99%AF)、[Wiki - 昇陽電腦](https://zh.wikipedia.org/wiki/%E6%98%87%E9%99%BD%E9%9B%BB%E8%85%A6))

雖然當年 Netscape 和發明 Java 的 Sun 公司之間有合作關係，使 JavaScript 在語法上受到 Java 影響和啟發。

但基本上 **Java 和 JavaScript 的關係就像太陽和太陽餅——沒有關係**。

### ECMAScript 等於 JavaScript？

1996 年 11 月，Netscape 公司決定將 JavaScript 提交給標準化組織 ECMA，希望這種語言能夠成為國際標準。

![](https://i.imgur.com/Ni37bmp.png)\
(Source: [Wiki - Ecma國際](https://zh.wikipedia.org/wiki/Ecma%E5%9B%BD%E9%99%85))

ECMA 是一家國際性會員制度的資訊和電信標準組織。1994年之前名為**歐洲電腦製造商協會** (**E**uropean **C**omputer **M**anufacturers **A**ssociation)。1994年之後，因為電腦的國際化，改名 Ecma 國際 (Ecma International)。

1997 年 6 月，ECMA 發布 ECMA-262 標準第一版，**以 JavaScript 語言為基礎，制定了瀏覽器腳本語言的標準，並將這種語言稱為 ECMAScript**，這就是 ES 初代目—— ECMAScript 1.0。

而 JavaScript 就是 ECMAScript 最著名的實現 (Implementation)。

所以嚴格來說，**ECMAScript 指的是一種標準規格 (Standard)，JavaScript 是這個標準的實現 (Implementation)**。除了 JavaScript，ActionScript 和 JScript 也都是 ECMAScript 標準的實現語言。

不過由於 JavaScript 的強勢發展，現在我們在溝通時，幾乎可以把 ECMAScript 和 JavaScript 劃上等號。

### ECMAScript 的成長史

ECMAScript 自 1997 年正式被發布為標準後，也持續有新版本制定。

以下是到今年為止的 JavaScript 版本歷史，參考就好：

| 版本  | 正式名稱            | 發布年份 | 備註   |
| --- | --------------- | ---- | ---- |
| 1   | ECMAScript 1    | 1997 | 第一版  |
| 2   | ECMAScript 2    | 1998 |      |
| 3   | ECMAScript 3    | 1999 |      |
| 4   | ECMAScript 4    | ---  | 從未發布 |
| 5   | ECMAScript 5    | 2009 |      |
| 5.1 | ECMAScript 5.1  | 2011 |      |
| 6   | ECMAScript 2015 | 2015 |      |
| 7   | ECMAScript 2016 | 2016 |      |
| 8   | ECMAScript 2017 | 2017 |      |
| 9   | ECMAScript 2018 | 2018 |      |

**從 ES6 開始正式名稱改用年份表示**，因此會看到「ES6」或「ES2015」這兩種說法。

可以注意到，從 2015 年開始突然很勤勞，每一年都有一個新版本發布。

### 年年擁抱新的 ECMAScript

ECMA 中負責制定 ECMAScript 標準的是**第 39 號技術專家委員會** (Technical Committee 39)，**簡稱 TC39**。

任何人都可以向 TC39 標準委員會提案，要求增修語法。

> ECMAScript 目前所有提案可以在 [TC39 的官方 GitHub](https://github.com/tc39/ecma262) 查看。

一個新的語法從提案到變成正式標準，需要經歷五個階段，每個階段都需要由 TC39 委員會批准：

* Stage 0: Strawman (展示階段)
* Stage 1: Proposal (提案階段)
* Stage 2: Draft (草案階段)
* Stage 3: Candidate (候選階段)
* Stage 4: Finished (定案階段)

一個提案只要能進入 Stage 2，幾乎就代表會被包含進未來的正式標準裡。所以有些工具會提供定案階段前的語法測試，例如[Babel 的線上 REPL](https://babeljs.io/repl)。

![](https://i.imgur.com/hifPNmy.png)\
(Source: [Babel REPL](https://babeljs.io/repl))

TC39 委員會想讓標準的升級成為常規流程，決定**在每年的 6 月份正式發布一次，作為當年度的正式版本**。接下來的時間就在這個版本的基礎上做增修，直到下一年的 6 月，草案自然變成新一年的版本，所以可以預期明年的 6 月就會有 ES10 誕生。

每年都有一個新版本發布是一個很快的速度，各家瀏覽器的實作支援要趕上自然會有落差，所以 Babel 這樣的轉譯工具更顯重要。這樣的發布速度也反映了 JavaScript 是何等活躍。

究竟 ES10 又會有什麼新特性，就讓我們拭目以待吧！

## 尾聲

這樣有意義的技術鐵人賽相信未來幾年都還會舉辦，很想勸世未來的勇者：一定要提早準備，不要像筆者自作孽。

標題帶點日文風味，是因為過去 30 天的感覺就像每天被追稿的連載漫畫家。

![](https://i.imgur.com/juNeeAW.png)\
(Source: [網路圖片](https://www.google.com.tw/search?q=%E6%BC%AB%E7%95%AB%E5%AE%B6+%E8%B6%95%E7%A8%BF\&rlz=1C1SAVA_enTW523TW523\&source=lnms\&tbm=isch\&sa=X\&ved=0ahUKEwin9KaM5s_eAhWBErwKHbsiAM8Q_AUIDigB\&biw=1280\&bih=882#imgrc=aAmXWBceBZbs2M:))

### 鐵人賽果然不是浪得虛名

![](https://i.imgur.com/eBaKKLW.png)\
(Source: [網路圖片](https://nos.netease.com/v163/snapshot/20180108/WaugI2823_cover.jpg))

這次參賽的決定頗為倉促，別說什麼庫存，開賽死線的前 3 天才決定報名、開始規劃主題大綱。報名時我心裡還想：哼，連續 30 天發文、每篇最低門檻才 300 字有什麼難，難道每天連一、兩個小時都撥不出來？

果然事情不是憨人想得這麼簡單。

![](https://i.imgur.com/wqbkXte.png)\
(Source: [網路圖片](https://komicolle.dreamhosters.com/pix/img12426.jpg))

技術文章和小說散文在寫作性質上有別，技術性質的內容本來就比較不易閱讀，需要花更多心力在文句修飾、編排潤飾，避免閱讀起來過於生硬乏味。

偏偏我又是屬於慢產型的寫作者，從發想、構思、將意念轉成草稿文字、編排文章脈絡、組織成條理性的敘述、想梗、潤飾、校稿，一篇文章花個五、六小時以上幾乎是基本盤，還不含技術研究的時間。

一天撥出一兩小時不難，每天撥出一兩小時就很具挑戰，何況還不是只要一兩小時。

生活也不會只有鐵人賽這一件事要忙，永遠無法知道何時會冒出事件需要處理，但一天只有 24 小時是肯定的。

在追稿狀態下要持續維持文章品質極難，回想過去 30 天的生活，扣掉一些必須處理的事件穿插，剩下的回憶幾乎是工作、吃飯、寫稿、熬夜、睡覺、工作、吃飯、寫稿、熬夜、睡覺，中間一度覺得自己快棄賽。

來到第 30 天，真的有種恍如隔世的感覺。

### 說好的 ES7、ES8 呢？

由於參賽倉促，沒時間學新東西，只好把不久前在學的 JavaScript 拿出來獻寶，看能不能炒出什麼好菜。

思考主題大綱時，本來考慮走新手教學路線，也就是規劃一份 30 天由淺入深的 JavaScript 入門教材，對象是從沒學過程式的人。

但後來覺得這件事在今年已經以現場程式教學的形式做過，雖然口頭教學和靜態教材是不同的挑戰，但在鐵人賽重複自己實在不太理想。沒辦法學新東西來參賽，還是希望在參賽過程能對自己有點收穫。

最後主題定位在針對 JavaScript 裡一些我認為比較混亂、抽象需要整理的觀念，或是比較新的用法，強化自己對 JavaScript 的掌握，所以 Day 1 文章還提到這 30 天的主題預計會包含 ES6、ES7、ES8 的新特性。

But，就是這個 But。

**有些概念在腦袋裡以為自己懂了是一回事，化成具體的口頭表達或文字描述確認自己真的懂了是另一回事，能講到別人也懂又是另一回事**。

為了將概念轉換為文字，需要更具體地去回顧細節，才發現自己可能還有某個觀念沒有釐清，本來以為很簡單的一個小觀念，越挖越深，細節竟然無窮無盡。

> 例如 Day 10 介紹了 Hoisting 觀念，本以為是一個相對簡單的小觀念，經邦友指正分享，才知道背後原理深不見底，精通二字談何容易。

不同觀念間又有先後關係，順序不對，閱讀節奏也會不順。

於是在 30 天的過程中，原先規劃的主題大綱不斷調整順序，一些本來沒在計畫內的主題也跳出來插隊。

這讓我想起作家倪匡在「我看金庸小說」一書裡，對「天龍八部」前言與小說後續發展不一的評語：

> 「寫作前的計畫、意願是一回事，寫出來的小說是怎麼樣的，又是另一回事。」 「計畫在創作的過程中，往往無法實踐，會中途突然改變，會有新的意念突然產生，會無法控制自己。」

沒想到這個系列最後只有帶到 ES6 少數特性，原本預計會提到的 ES6 解構賦值、Promises、Classes、Module，甚至 ES8 的 async/await，通通都沒有～

![](https://i.imgur.com/ynA5XVD.png)\
(Source: [Youtube](https://www.youtube.com/watch?v=HzXLJyHLStc))

還好，就像 Day 1 文章提到，近年 JavaScript 果然是個大明星。放眼望去今年的鐵人賽參賽主題，以 JavaScript 為題的應該可以湊個三五桌麻將，別說 ES5、ES6，甚至不乏 ES8、ES9。

![](https://i.imgur.com/F9nXAet.png)\
(Source: [Youtube](https://www.youtube.com/watch?v=b3op8Vo00Ec))

既然如此，新版本的語法介紹也不差我一個湊熱鬧。與其把一堆觀念以沾醬油的方式介紹過一輪，不如好好做個深度整理。不求主題涵蓋範圍包山包海，但求釐清一些微妙的概念。

就成果來說筆者自己還算欣慰，在維持不令自己汗顏的文章品質前提，不敢說多有深度，至少不是走馬看花淺淺帶過，多少釐清了一些抽象觀念，甚至糾正了自己原先理解錯誤的觀念。

**最後，感謝訂閱這個系列的邦友、以及正看著這篇文章的您，筆者能撐過血尿的 30 天，你們的支持是十分重要的鼓勵。**

就讓我用 Day 7 的一段範例程式碼來為這個系列劃下句點：

```javascript
// Day 7
var gameName = "IT Help 2019";
var gamer = "OneJar", topic = "Something about JavaScript", progress = 7;
var isFinished; // A variable declared without a value will have the value **undefined**.

// Day 30
progress = 30;
if(progress == 30){
    isFinished = true;
    console.log('Thank you guys!');
}
```

## References

* [W3Schools - JavaScript Versions](https://www.w3schools.com/js/js_versions.asp)
* [ECMAScript 6 入門](https://es6.ruanyifeng.com/#docs/intro)
* [「译」ES5, ES6, ES2016, ES.Next: JavaScript 的版本是怎么回事？](https://huangxuan.me/2015/09/22/js-version/)
* [JavaScript 的历史](https://www.w3school.com.cn/js/pro_js_history.asp)
* [Wiki - 網景](https://zh.wikipedia.org/wiki/%E7%B6%B2%E6%99%AF)
* [Wiki - 昇陽電腦](https://zh.wikipedia.org/wiki/%E6%98%87%E9%99%BD%E9%9B%BB%E8%85%A6)
* [Wiki - Ecma國際](https://zh.wikipedia.org/wiki/Ecma%E5%9B%BD%E9%99%85)
* [Wiki - ECMAScript](https://zh.wikipedia.org/wiki/ECMAScript)
* [Wiki - JavaScript](https://zh.wikipedia.org/wiki/JavaScript)


