- hw1:Event Loop
- hw2:Event Loop + Scope
- hw3:Hoisting
- hw4:What is this?
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)
輸出結果
1
3
5
2
4
執行流程
- 將
console.log(1)
放入 Call Stack 並直接執行,印出 1,執行結束後移除 - 將
setTimeout(() => { console.log(2) }, 0)
放入 Call Stack,透過 Web API,在瀏覽器設定計時器為 0,直到倒數結束,將() => { console.log(2) }
放到 Callback Queue 等待執行,setTimeout 執行結束後從 Call Stack 移除 - 將
console.log(3)
放入 Call Stack 並直接執行,印出 3,執行結束後移除 - 將
setTimeout(() => { console.log(4) }, 0)
放入 Call Stack,透過 Web API,在瀏覽器設定計時器為 0,直到倒數結束,將() => { console.log(4) }
放到 Callback Queue 等待執行,setTimeout 執行結束後從 Call Stack 移除 - 將
console.log(5)
放入 Call Stack 並直接執行,印出 5,執行結束後移除 - 當 Event Loop 偵測到 call stack 為空時,依序將 Callback Queue 的任務丟到 Call Stack 執行
- 執行
() => { console.log(2) }
,再執行console.log(2)
,印出 2,執行結束後移除 - 接著執行
() => { console.log(4) }
,再執行console.log(4)
,印出 4,執行結束後移除
hw2:Event Loop + Scope
請說明以下程式碼會輸出什麼,以及盡可能詳細地解釋原因。
for(var i=0; i<5; i++) {
console.log('i: ' + i)
setTimeout(() => {
console.log(i)
}, i * 1000)
}
輸出結果
i: 0
i: 1
i: 2
i: 3
i: 4
5
5
5
5
5
執行流程
- 將 for 迴圈放入 Call Stack 並開始執行,宣告變數 i = 0,判斷 i 是否小於 5,是,進入第一圈迴圈
- 將
console.log('i: ' + 0)
放入 Call Stack 並直接執行,印出 i: 0 - 將
setTimeout(() => { console.log(0) }, 0 * 1000)
放入 Call Stack,透過 Web API,在瀏覽器設定計時器為 0 ms,直到倒數結束,將() => { console.log(0) }
放到 Callback Queue 等待執行,setTimeout 執行結束後從 Call Stack 移除 - 第一圈迴圈結束,將 i + 1
- i = 1,判斷 i 是否小於 5,是,進入第二圈迴圈
- 將
console.log('i: ' + 1)
放入 Call Stack 並直接執行,印出 i: 1 - 將
setTimeout(() => { console.log(i) }, 1 * 1000)
放入 Call Stack,透過 Web API,在瀏覽器設定計時器為 1000 ms ,直到倒數結束,將() => { console.log(i) }
放到 Callback Queue 等待執行,setTimeout 執行結束後從 Call Stack 移除 - 第二圈迴圈結束,將 i + 1
- i = 2,判斷 i 是否小於 5,是,進入第二圈迴圈
- 將
console.log('i: ' + 2)
放入 Call Stack 並直接執行,印出 i: 2 - 將
setTimeout(() => { console.log(i) }, 2 * 1000)
放入 Call Stack,透過 Web API,在瀏覽器設定計時器為 2000 ms ,直到倒數結束,將() => { console.log(i) }
放到 Callback Queue 等待執行,setTimeout 執行結束後從 Call Stack 移除 - 第二圈迴圈結束,將 i + 1
- i = 3,判斷 i 是否小於 5,是,進入第三圈迴圈
- 將
console.log('i: ' + 3)
放入 Call Stack 並直接執行,印出 i: 3 - 將
setTimeout(() => { console.log(i) }, 3 * 1000)
放入 Call Stack,透過 Web API,在瀏覽器設定計時器為 3000 ms ,直到倒數結束,將() => { console.log(i) }
放到 Callback Queue 等待執行,setTimeout 執行結束後從 Call Stack 移除 - 第三圈迴圈結束,將 i + 1
- i = 4,判斷 i 是否小於 5,是,進入第四圈迴圈
- 將
console.log('i: ' + 4)
放入 Call Stack 並直接執行,印出 i: 4 - 將
setTimeout(() => { console.log(i) }, 4 * 1000)
放入 Call Stack,透過 Web API,在瀏覽器設定計時器為 4000 ms ,直到倒數結束,將() => { console.log(i) }
放到 Callback Queue 等待執行,setTimeout 執行結束後從 Call Stack 移除 - 第四圈迴圈結束,將 i + 1
- i = 5,判斷 i 是否小於 5,否,跳出迴圈,執行結束後從 Call Stack 移除
- 當 Event Loop 偵測到 call stack 為空時,依序將 Callback Queue 的任務丟到 Call Stack 執行
- 執行第一個
() => { console.log(i) }
,再執行console.log(i)
,在 function 的 EC 中找不到 i,往上一層 EC 找,找到 i = 5,印出 5,執行結束從 Call Stack 移除 - 執行第二個
() => { console.log(i) }
,再執行console.log(i)
,在 function 的 EC 中找不到 i,往上一層 EC 找,找到 i = 5,印出 5,執行結束從 Call Stack 移除 - 執行第三個
() => { console.log(i) }
,再執行console.log(i)
,在 function 的 EC 中找不到 i,往上一層 EC 找,找到 i = 5,印出 5,執行結束從 Call Stack 移除 - 執行第四個
() => { console.log(i) }
,再執行console.log(i)
,在 function 的 EC 中找不到 i,往上一層 EC 找,找到 i = 5,印出 5,執行結束從 Call Stack 移除 - 執行第五個
() => { console.log(i) }
,再執行console.log(i)
,在 function 的 EC 中找不到 i,往上一層 EC 找,找到 i = 5,印出 5,執行結束從 Call Stack 移除
hw3:Hoisting
請說明以下程式碼會輸出什麼,以及盡可能詳細地解釋原因。
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
輸出結果
undefined
5
6
20
1
10
100
執行流程
- 開始執行程式,建立 global EC 並初始化 VO
global EC
VO {
fn: function,
a: undefined
}
}
- 執行第一行程式碼,宣告變數 a 並賦值為 1
global EC {
VO {
fn: function,
a: 1
}
}
- 呼叫 fn(),建立 fn EC 並初始化 AO,變數宣告會提升
var = a
fn EC {
AO {
fn2: function,
a: undefined
}
}
- 進入 function fn 並執行 console.log(a),找到 fn AO 中 a = undefined,印出 undefined
- 執行 var a = 5,查看 fn EC 是否有 a,找到 a,將 a 賦值為 5
fn EC {
AO {
fn2: function,
a: 5
}
}
- 執行 console.log(a),找到 fn AO 中 a = 5,印出 5
- 執行 a++,查看 fn EC 是否有 a,將 a 賦值為 6
fn EC {
AO {
fn2: function,
a: 6
}
}
- 已經宣告過變數 a,忽略
var a
- 呼叫 fn2(),建立 fn EC 並初始化 AO
fn2 EC {
AO {
// 沒有進行任何宣告
}
}
- 進入 function fn2 並執行 console.log(a),查看 fn2 AO 沒有找到 a;往上一層 fn AO 找,找到 a = 6,印出 6
- 執行 a = 20,在 fn2 AO 沒有找到 a;往上一層 fn AO 找,找到 a,並賦值 a 為 20
fn EC {
AO {
fn2: function,
a: 20
}
}
- 執行 b = 100,在 fn2 AO 沒有找到 b;往上一層 fn AO 找,沒有找到 b;再往上一層 global VO 找,沒有找到 b。因為是在非嚴格模式執行程式碼,會在 global VO 宣告變數 b 並賦值為 100
global EC
VO {
fn: function,
a: 1
b: 100
}
}
- function fn2 執行結束,移除 fn2 EC,回到 fn EC 執行其餘程式碼
- 執行 console.log(a),找到 fn AO 中 a = 20,印出 20
- function fn 執行結束,移除 fn EC,回到 global EC 執行其餘程式碼
- 執行 console.log(a),找到 global VO 中 a = 1,印出 1
- 執行 a = 10,查看 global EC 是否有 a,找到 a,將 a 賦值為 10
global EC
VO {
fn: function,
a: 10
b: 100
}
}
- 執行 console.log(a),找到 global VO 中 a = 10,印出 10
- 執行 console.log(b),找到 global VO 中 b = 100,印出 100
hw4:What is this?
請說明以下程式碼會輸出什麼,以及盡可能詳細地解釋原因。
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
輸出結果
2
2
undefined
執行流程
obj.inner.hello()
可看成 .call()
的形式:obj.inner.hello.call(obj.inner)
,this 會是傳入的參數,也就是 obj.inner
,因此 obj.inner.value
得到的結果是 2。
obj2.hello()
和上一題相同,可看成 .call()
的形式:obj2.hello.call(obj2)
,this 就會是 obj2
,又因 obj2 = obj.inner
,因此結果同樣會是 2。
hello()
在不需要的地方呼叫 this 時,this 會被指定為全域物件。依照執行環境不同,其值也會改變,例如在瀏覽器執行會是 Window,在 node.js 執行則是會是 Global。
若是在'use strict';
(嚴格模式)下執行,this 的值會是 undefined。
這週學了一大堆以前搞不懂的東西,你有變得更懂了嗎?請寫下你的心得。
這週的學習筆記
- [week 16] JavaScript 進階 - 關於變數與資料型態
- [week 16] JavaScript 進階 - 初探 Hoisting & Execution Context
- [week 16] 淺談 JavaScript:同步與非同步 & Callback Function & Event Loop
- [week 16] JavaScript 進階 - 什麼是閉包?探討 Closure & Scope Chain
- [week 16] JavaScript 進階 - 物件導向 & Prototype
- [week 16] JavaScript 進階 - What is this?
學習心得
這一週的知識量其實蠻大的,從複習 JavaScript 的變數與資料型態,等號賦值與記憶體位置等等,在第二週的課程也有提到相關概念,到了第十六週則是要去瞭解程式背後是如何運作的。
Hoisting & Execution Contexts & Variable Object
從理解什麼是 Hoisting(提升),瞭解我們為什麼需要提升,再延伸到運作原理。過程中建立的 Execution Contexts(執行環境)、與之對應的 Variable Object(變數物件)等等,其實涉及到有關 JavaScript 的範圍非常廣。
除了課堂影片提到的內容,自己也上網查了許多有關執行環境、執行堆疊的資料,雖然花費不少時間,卻也藉由瞭解 JavaScript 的編譯與執行過程,從建立到執行階段,加深對整個架構的理解。
Event Loop
在閱讀完 JavaScript 中的同步與非同步(上):先成為 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 有很大的關聯。此外,物件導向其實應用在許多現代的程式語言,以物件導向的方式進行開發。
物件導向程式的寫法,基本上可分為三部分:
- 定義物件類別(class)。例如:
class Dog
- 定義物件類別中的屬性與方法。例如:可使用
dog.name
存取屬性,使用dog.sayHello()
存取方法 - 定義物件之間的行為,也就是主程式
之所以需要物件導向,最重要的目的就是把資料(屬性)與函式(方法)結合在一起,定義出物件模型,這麼做有幾個優點:
- 便於重複使用程式碼
- 能夠隱藏程式內部資訊
- 透過模組化來簡化主程式邏輯
而這些概念,其實也就是先前談到有關物件導向的三大特性,並且三者具有次序性,沒有封裝就不可能有繼承、沒有繼承就不可能有多型:
- 封裝(Encapsulation):
- 藉由把程式包成類別,能夠隱藏物件內容
- 避免程式間互相干擾,也利於後續維護
- 繼承(Inheritance):
- 子層能夠繼承使用父層的屬性和方法,並且加以微調
- 能夠重複使用程式碼
- 多型(Polymorphism):
- 父層可透過子層衍伸成多種型態,接著子層可藉由覆寫父層的方法來達到多型
- 可增加程式架構的彈性與維護性
藉由瞭解什麼是物件導向,為什麼需要物件導向以後,對整體架構似乎又更加清楚一些。過程中也查了許多資料,在碰到新的名詞時總會感到慌張,像是 constructor(建構子)、prototype(原型)、instance(實例)等等,其實只要能夠先瞭解定義是什麼,就不難繼續理解整體架構。
最後,在找相關資料的時候,有在這篇網誌中,看到使用泡麵的例子來比喻物件導向,因為還蠻喜歡的也記錄在這裡:
- 由泡麵工廠製作麵和醬包,並包裝在一起,我們可以直接買來享用
- 我們可以在泡麵中自己加料,或是不用泡的改用炒的
- 同樣都是泡麵,卻能夠實作出不同的口味
What is this?
瞭解到物件導向的相關概念後,接著要理解 this 是什麼就沒那麼困難了。或許是因為在實際學 JacaScript 以前,就預設 this 是很難是高手在用的東西,透過慢慢理解物件導向與 this 的關聯,以及如何判斷 this 的值,似乎也感覺到自己的進化,對於未知的恐懼總是需要克服的。
關於 this 的重點,就是記得 this 的值和程式碼在哪無關,而是和怎麼呼叫有關係。
總結前面提到的觀念,其實 this 大致可分成四種綁定方式:
- 默認綁定
在和物件導向無關的情況下,this 會被指定為全域物件。又依照執行環境不同,其值會是 global 或 window,而在嚴格模式下會是 undefined:
function test() {
console.log(this); // Window
}
test();
- 隱式綁定
若在 function 中, this 有被某物件指定為屬性並呼叫,this 就是呼叫 function 的物件。以下方範例來說 this 就是 obj:
function func() {
console.log(this.a);
}
var obj = {
a: 4,
test: func
};
obj.test(); // 4
- 顯示綁定
若是透過 .call()
、.apply()
或 .bind()
方式指定 this,this 就會是傳入的參數:
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 物件本身:
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 過一遍,大致瞭解背後是如何運作,也把過去一些錯誤的觀念改正,或是終於瞭解為什麼以前想賦值給某個變數時,沒有辦法改動值等等。不過理解觀念是一回事,重要的還是如何實際應用,之後實作時也要來試著運用物件導向的概念去寫程式。
再來要繼續往下一週邁進了,繼續努力!