# [week 16] JavaScript 進階 - 作業練習
## hw1:Event Loop
在 JavaScript 裡面,一個很重要的概念就是 Event Loop,是 JavaScript 底層在執行程式碼時的運作方式。請你說明以下程式碼會輸出什麼,以及盡可能詳細地解釋原因。
```
console.log(1)
setTimeout(() => {
console.log(2)
}, 0)
console.log(3)
setTimeout(() => {
console.log(4)
}, 0)
console.log(5)
```
### 輸出結果
```js
1
3
5
2
4
```
### 執行流程
1. 將 `console.log(1)` 放入 Call Stack 並直接執行,印出 1,執行結束後移除
2. 將 `setTimeout(() => { console.log(2) }, 0)` 放入 Call Stack,透過 Web API,在瀏覽器設定計時器為 0,直到倒數結束,將 `() => { console.log(2) }` 放到 Callback Queue 等待執行,setTimeout 執行結束後從 Call Stack 移除
3. 將 `console.log(3)` 放入 Call Stack 並直接執行,印出 3,執行結束後移除
4. 將 `setTimeout(() => { console.log(4) }, 0)` 放入 Call Stack,透過 Web API,在瀏覽器設定計時器為 0,直到倒數結束,將 `() => { console.log(4) }` 放到 Callback Queue 等待執行,setTimeout 執行結束後從 Call Stack 移除
5. 將 `console.log(5)` 放入 Call Stack 並直接執行,印出 5,執行結束後移除
6. 當 Event Loop 偵測到 call stack 為空時,依序將 Callback Queue 的任務丟到 Call Stack 執行
7. 執行 `() => { console.log(2) }`,再執行 `console.log(2)`,印出 2,執行結束後移除
8. 接著執行 `() => { console.log(4) }`,再執行 `console.log(4)`,印出 4,執行結束後移除
---
## hw2:Event Loop + Scope
請說明以下程式碼會輸出什麼,以及盡可能詳細地解釋原因。
```js
for(var i=0; i<5; i++) {
console.log('i: ' + i)
setTimeout(() => {
console.log(i)
}, i * 1000)
}
```
### 輸出結果
```js
i: 0
i: 1
i: 2
i: 3
i: 4
5
5
5
5
5
```
### 執行流程
1. 將 for 迴圈放入 Call Stack 並開始執行,宣告變數 i = 0,判斷 i 是否小於 5,是,進入第一圈迴圈
2. 將 `console.log('i: ' + 0)` 放入 Call Stack 並直接執行,印出 i: 0
3. 將 `setTimeout(() => { console.log(0) }, 0 * 1000)` 放入 Call Stack,透過 Web API,在瀏覽器設定計時器為 0 ms,直到倒數結束,將 `() => { console.log(0) }` 放到 Callback Queue 等待執行,setTimeout 執行結束後從 Call Stack 移除
4. 第一圈迴圈結束,將 i + 1
5. i = 1,判斷 i 是否小於 5,是,進入第二圈迴圈
6. 將 `console.log('i: ' + 1)` 放入 Call Stack 並直接執行,印出 i: 1
7. 將 `setTimeout(() => { console.log(i) }, 1 * 1000)` 放入 Call Stack,透過 Web API,在瀏覽器設定計時器為 1000 ms ,直到倒數結束,將 `() => { console.log(i) }` 放到 Callback Queue 等待執行,setTimeout 執行結束後從 Call Stack 移除
8. 第二圈迴圈結束,將 i + 1
9. i = 2,判斷 i 是否小於 5,是,進入第二圈迴圈
10. 將 `console.log('i: ' + 2)` 放入 Call Stack 並直接執行,印出 i: 2
11. 將 `setTimeout(() => { console.log(i) }, 2 * 1000)` 放入 Call Stack,透過 Web API,在瀏覽器設定計時器為 2000 ms ,直到倒數結束,將 `() => { console.log(i) }` 放到 Callback Queue 等待執行,setTimeout 執行結束後從 Call Stack 移除
12. 第二圈迴圈結束,將 i + 1
13. i = 3,判斷 i 是否小於 5,是,進入第三圈迴圈
14. 將 `console.log('i: ' + 3)` 放入 Call Stack 並直接執行,印出 i: 3
15. 將 `setTimeout(() => { console.log(i) }, 3 * 1000)` 放入 Call Stack,透過 Web API,在瀏覽器設定計時器為 3000 ms ,直到倒數結束,將 `() => { console.log(i) }` 放到 Callback Queue 等待執行,setTimeout 執行結束後從 Call Stack 移除
16. 第三圈迴圈結束,將 i + 1
17. i = 4,判斷 i 是否小於 5,是,進入第四圈迴圈
18. 將 `console.log('i: ' + 4)` 放入 Call Stack 並直接執行,印出 i: 4
19. 將 `setTimeout(() => { console.log(i) }, 4 * 1000)` 放入 Call Stack,透過 Web API,在瀏覽器設定計時器為 4000 ms ,直到倒數結束,將 `() => { console.log(i) }` 放到 Callback Queue 等待執行,setTimeout 執行結束後從 Call Stack 移除
20. 第四圈迴圈結束,將 i + 1
21. i = 5,判斷 i 是否小於 5,否,跳出迴圈,執行結束後從 Call Stack 移除
22. 當 Event Loop 偵測到 call stack 為空時,依序將 Callback Queue 的任務丟到 Call Stack 執行
23. 執行第一個 `() => { console.log(i) }`,再執行 `console.log(i)`,在 function 的 EC 中找不到 i,往上一層 EC 找,找到 i = 5,印出 5,執行結束從 Call Stack 移除
24. 執行第二個 `() => { console.log(i) }`,再執行 `console.log(i)`,在 function 的 EC 中找不到 i,往上一層 EC 找,找到 i = 5,印出 5,執行結束從 Call Stack 移除
25. 執行第三個 `() => { console.log(i) }`,再執行 `console.log(i)`,在 function 的 EC 中找不到 i,往上一層 EC 找,找到 i = 5,印出 5,執行結束從 Call Stack 移除
26. 執行第四個 `() => { console.log(i) }`,再執行 `console.log(i)`,在 function 的 EC 中找不到 i,往上一層 EC 找,找到 i = 5,印出 5,執行結束從 Call Stack 移除
27. 執行第五個 `() => { console.log(i) }`,再執行 `console.log(i)`,在 function 的 EC 中找不到 i,往上一層 EC 找,找到 i = 5,印出 5,執行結束從 Call Stack 移除
---
## hw3:Hoisting
請說明以下程式碼會輸出什麼,以及盡可能詳細地解釋原因。
```js
var a = 1
function fn() {
console.log(a) // undefined
var a = 5
console.log(a) // 5
a++
var a
fn2()
console.log(a) // 6
function fn2() {
console.log(a) // 20
a = 20
b = 100
}
}
fn()
console.log(a) // 1
a = 10
console.log(a) // 10
console.log(b) // 100
```
### 輸出結果
```js
undefined
5
6
20
1
10
100
```
### 執行流程
1. 開始執行程式,建立 global EC 並初始化 VO
```js
global EC
VO {
fn: function,
a: undefined
}
}
```
2. 執行第一行程式碼,宣告變數 a 並賦值為 1
```js
global EC {
VO {
fn: function,
a: 1
}
}
```
3. 呼叫 fn(),建立 fn EC 並初始化 AO,變數宣告會提升 `var = a`
```js
fn EC {
AO {
fn2: function,
a: undefined
}
}
```
4. 進入 function fn 並執行 console.log(a),找到 fn AO 中 a = undefined,印出 undefined
5. 執行 var a = 5,查看 fn EC 是否有 a,找到 a,將 a 賦值為 5
```js
fn EC {
AO {
fn2: function,
a: 5
}
}
```
6. 執行 console.log(a),找到 fn AO 中 a = 5,印出 5
7. 執行 a++,查看 fn EC 是否有 a,將 a 賦值為 6
```js
fn EC {
AO {
fn2: function,
a: 6
}
}
```
8. 已經宣告過變數 a,忽略 `var a`
9. 呼叫 fn2(),建立 fn EC 並初始化 AO
```js
fn2 EC {
AO {
// 沒有進行任何宣告
}
}
```
10. 進入 function fn2 並執行 console.log(a),查看 fn2 AO 沒有找到 a;往上一層 fn AO 找,找到 a = 6,印出 6
11. 執行 a = 20,在 fn2 AO 沒有找到 a;往上一層 fn AO 找,找到 a,並賦值 a 為 20
```js
fn EC {
AO {
fn2: function,
a: 20
}
}
```
12. 執行 b = 100,在 fn2 AO 沒有找到 b;往上一層 fn AO 找,沒有找到 b;再往上一層 global VO 找,沒有找到 b。因為是在非嚴格模式執行程式碼,會在 global VO 宣告變數 b 並賦值為 100
```js
global EC
VO {
fn: function,
a: 1
b: 100
}
}
```
13. function fn2 執行結束,移除 fn2 EC,回到 fn EC 執行其餘程式碼
14. 執行 console.log(a),找到 fn AO 中 a = 20,印出 20
15. function fn 執行結束,移除 fn EC,回到 global EC 執行其餘程式碼
16. 執行 console.log(a),找到 global VO 中 a = 1,印出 1
17. 執行 a = 10,查看 global EC 是否有 a,找到 a,將 a 賦值為 10
```js
global EC
VO {
fn: function,
a: 10
b: 100
}
}
```
18. 執行 console.log(a),找到 global VO 中 a = 10,印出 10
19. 執行 console.log(b),找到 global VO 中 b = 100,印出 100
---
## hw4:What is this?
請說明以下程式碼會輸出什麼,以及盡可能詳細地解釋原因。
```js
const obj = {
value: 1,
hello: function() {
console.log(this.value)
},
inner: {
value: 2,
hello: function() {
console.log(this.value)
}
}
}
const obj2 = obj.inner
const hello = obj.inner.hello
obj.inner.hello() // 2
obj2.hello() // 2
hello() // undefined
```
### 輸出結果
```js
2
2
undefined
```
### 執行流程
1. `obj.inner.hello()`
可看成 `.call()` 的形式:`obj.inner.hello.call(obj.inner)`,this 會是傳入的參數,也就是 `obj.inner`,因此 `obj.inner.value` 得到的結果是 2。
2. `obj2.hello()`
和上一題相同,可看成 `.call()` 的形式:`obj2.hello.call(obj2)`,this 就會是 `obj2`,又因 `obj2 = obj.inner`,因此結果同樣會是 2。
3. `hello()`
在不需要的地方呼叫 this 時,this 會被指定為全域物件。依照執行環境不同,其值也會改變,例如在瀏覽器執行會是 Window,在 node.js 執行則是會是 Global。
若是在`'use strict';`(嚴格模式)下執行,this 的值會是 undefined。
## 這週學了一大堆以前搞不懂的東西,你有變得更懂了嗎?請寫下你的心得。
### 這週的學習筆記
- [[week 16] JavaScript 進階 - 關於變數與資料型態](https://hackmd.io/@Heidi-Liu/note-js201-data-type)
- [[week 16] JavaScript 進階 - 初探 Hoisting & Execution Context](https://hackmd.io/@Heidi-Liu/note-js201-hoisting)
- [[week 16] 淺談 JavaScript:同步與非同步 & Callback Function & Event Loop](https://hackmd.io/@Heidi-Liu/note-javascript-callback)
- [[week 16] JavaScript 進階 - 什麼是閉包?探討 Closure & Scope Chain](https://hackmd.io/@Heidi-Liu/note-js201-closure)
- [[week 16] JavaScript 進階 - 物件導向 & Prototype](https://hackmd.io/@Heidi-Liu/note-js201-oop-prototype)
- [[week 16] JavaScript 進階 - What is this?](https://hackmd.io/@Heidi-Liu/note-js201-this)
### 學習心得
這一週的知識量其實蠻大的,從複習 JavaScript 的變數與資料型態,等號賦值與記憶體位置等等,在第二週的課程也有提到相關概念,到了第十六週則是要去瞭解程式背後是如何運作的。
#### Hoisting & Execution Contexts & Variable Object
從理解什麼是 Hoisting(提升),瞭解我們為什麼需要提升,再延伸到運作原理。過程中建立的 Execution Contexts(執行環境)、與之對應的 Variable Object(變數物件)等等,其實涉及到有關 JavaScript 的範圍非常廣。
除了課堂影片提到的內容,自己也上網查了許多有關執行環境、執行堆疊的資料,雖然花費不少時間,卻也藉由瞭解 JavaScript 的編譯與執行過程,從建立到執行階段,加深對整個架構的理解。
#### Event Loop
在閱讀完 [JavaScript 中的同步與非同步(上):先成為 callback 大師吧!](https://blog.huli.tw/2019/10/04/javascript-async-sync-and-callback/) 這篇文章,原本對 callback 概念薄弱的自己,對同步與非同步又有了新的一層認識。
尤其是才剛接著學完有關 Hoisting 的運作原理,在瞭解什麼是執行環境以後,再回來看 Event Loop 似乎也更能夠理解當中的執行流程。
也想到再次看到 Node.js 是 JavaScript 的 runtime(執行環境)這句話時,會想到 Execution Context 的中文也被翻成執行環境,但其實兩者指的對象不同。前者指的是「執行時系統」(run-time system);後者指的是 JavaScript 在執行時會建立的環境,又可分為全域與函式執行環境。翻成中文的壞處就是容易撞名混淆,還是讓自己盡量去理解原文的意思。
#### Closure & Scope
在學到 Closure(閉包)時,發現其實花了很多時間在瞭解有關 Scope(作用域)的概念。也是在這一單元瞭解到,原來之前在課程中學到的非同步操作,當中的 callback 其實就和閉包有關,有關 callback 的觀念真的非常重要,也難怪這些觀念會不斷在課程中被提到。
此外也瞭解到,閉包在框架中很常會使用到,透過閉包的方式,就能夠避免汙染全域變數或是記憶體洩漏等問題。
一開始之所以沒辦法很快理解,或許就是沒有把這些觀念融會貫通,都是一個環節接著另一個環節,就和 Scope Chain 一樣,會需要往上一層去找出需要的拼圖。
#### 物件導向 & prototype
其實在學習 JavsScript 之前,一直以為物件導向和 this 是能夠畫上等號的(三個的那種)。直到實際學到物件導向以後,才瞭解到物件導向中有許多觀念,其實和在之前學到的 Hoisting、Closure 有很大的關聯。此外,物件導向其實應用在許多現代的程式語言,以物件導向的方式進行開發。
物件導向程式的寫法,基本上可分為三部分:
1. 定義物件類別(class)。例如:`class Dog`
2. 定義物件類別中的屬性與方法。例如:可使用 `dog.name` 存取屬性,使用 `dog.sayHello()` 存取方法
3. 定義物件之間的行為,也就是主程式
之所以需要物件導向,最重要的目的就是把資料(屬性)與函式(方法)結合在一起,定義出物件模型,這麼做有幾個優點:
- 便於重複使用程式碼
- 能夠隱藏程式內部資訊
- 透過模組化來簡化主程式邏輯
而這些概念,其實也就是先前談到有關物件導向的三大特性,並且三者具有次序性,沒有封裝就不可能有繼承、沒有繼承就不可能有多型:
- 封裝(Encapsulation):
- 藉由把程式包成類別,能夠隱藏物件內容
- 避免程式間互相干擾,也利於後續維護
- 繼承(Inheritance):
- 子層能夠繼承使用父層的屬性和方法,並且加以微調
- 能夠重複使用程式碼
- 多型(Polymorphism):
- 父層可透過子層衍伸成多種型態,接著子層可藉由覆寫父層的方法來達到多型
- 可增加程式架構的彈性與維護性
藉由瞭解什麼是物件導向,為什麼需要物件導向以後,對整體架構似乎又更加清楚一些。過程中也查了許多資料,在碰到新的名詞時總會感到慌張,像是 constructor(建構子)、prototype(原型)、instance(實例)等等,其實只要能夠先瞭解定義是什麼,就不難繼續理解整體架構。
最後,在找相關資料的時候,有在這篇[網誌](https://igouist.github.io/post/2020/07/oo-5-polymorphism/)中,看到使用泡麵的例子來比喻物件導向,因為還蠻喜歡的也記錄在這裡:
1. 由泡麵工廠製作麵和醬包,並包裝在一起,我們可以直接買來享用
2. 我們可以在泡麵中自己加料,或是不用泡的改用炒的
3. 同樣都是泡麵,卻能夠實作出不同的口味
#### What is this?
瞭解到物件導向的相關概念後,接著要理解 this 是什麼就沒那麼困難了。或許是因為在實際學 JacaScript 以前,就預設 this 是很難是高手在用的東西,透過慢慢理解物件導向與 this 的關聯,以及如何判斷 this 的值,似乎也感覺到自己的進化,對於未知的恐懼總是需要克服的。
關於 this 的重點,就是記得 this 的值和程式碼在哪無關,而是和怎麼呼叫有關係。
總結前面提到的觀念,其實 this 大致可分成四種綁定方式:
- 默認綁定
在和物件導向無關的情況下,this 會被指定為全域物件。又依照執行環境不同,其值會是 global 或 window,而在嚴格模式下會是 undefined:
```javascript=
function test() {
console.log(this); // Window
}
test();
```
- 隱式綁定
若在 function 中, this 有被某物件指定為屬性並呼叫,this 就是呼叫 function 的物件。以下方範例來說 this 就是 obj:
```javascript=
function func() {
console.log(this.a);
}
var obj = {
a: 4,
test: func
};
obj.test(); // 4
```
- 顯示綁定
若是透過 `.call()`、`.apply()` 或 `.bind()` 方式指定 this,this 就會是傳入的參數:
```javascript=
var obj = {
a: 10,
test: function () {
console.log(this);
}
}
obj.test.call(obj)
obj.test.apply(obj)
// 第一種寫法:直接呼叫 function
obj.test.bind(obj)();
// 第二種寫法:先宣告,再呼叫
const bindTest = obj.test.bind(obj);
bindTest();
// 均印出: { a: 10, test: [Function: test] }
```
- new 綁定
透過建構函式 new 出一個 instance,this 就會是 instance 物件本身:
```javascript=
class Dog {
constructor(name) {
this.name = name;
console.log(this); // Dog {name: "dog A"}
console.log(this.name); // dog A
}
}
var a = new Dog('dog A');
```
- 例外:箭頭函式中的 this 是看程式碼定義在哪,和怎麼呼叫沒關係。
#### 總結
終於學到傳說中的物件導向,以及面對 JavaScript 中的大魔王 this。還記得在開始程式導師計畫之前,有在 Udemy 買過 JavaScript: Understanding the Weird Parts(中譯:JavaScript 全攻略:克服 JS 的奇怪部分)這堂課,但其實那時候也沒看多少,現在想想當初連基礎都還沒打穩,難怪會不知道自己在聽什麼XD。上網查過資料會發現蠻多類似的標題,不外乎是「你所不知道的 JS」、「其實 JS 跟你想的不一樣」等等,所以 JavaScript 到底是怪在哪?!在學完 JavaScript 基礎之後,還只是理解這個程式語言的皮毛而已。
把這一週的筆記整理完,寫作業的時候也感覺踏實多了,總算是釐清 Event Loop、Hoisting、Closure、物件導向和 this 等相關概念。或許是因為看到新名詞時總會感到害怕,會忍不住去查定義,查為什麼要這樣用,不這樣用又會有什麼影響等等,好像要先完全掌握這些名詞的意義以後,才能在繼續再下一步前進。
但實際上,在嘗試理解的過程中,有很重要的一點,就是「實作」。與其查了一堆定義和文字一翻兩瞪眼,倒不如跟著課程範例操作,實際在程式跑過一遍,知道會有怎樣的結果以後,才能理解文字的意義,然後再去試著自己變化程式碼,看看結果有沒有和自己想的一樣,到最後就差不多能夠自己寫出簡單的範例來了。
硬是要把提升、閉包、物件導向或是一些方法定義背起來,其實也記不久,看過就忘了,想想這其實也是自己的壞習慣,在還沒理解之前會想著乾脆先記起來,但隨著要學習的東西越深越廣,再用這種方法實在不是長久之計,直接來個範例吧!是最近有關學習的體悟,之後也要謹記這件事情。
總之,終於把 JavaScript 進階的相關觀念都 Run 過一遍,大致瞭解背後是如何運作,也把過去一些錯誤的觀念改正,或是終於瞭解為什麼以前想賦值給某個變數時,沒有辦法改動值等等。不過理解觀念是一回事,重要的還是如何實際應用,之後實作時也要來試著運用物件導向的概念去寫程式。
再來要繼續往下一週邁進了,繼續努力!