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
全域環境程式(這裡稱作
main()
)進入 call stackconsole.log(1)
被 push 進 call stack 的最上方印出
1
將
console.log(1)
從 call stack 最上方移除setTimeout()
進入 call stack 最上方,讓瀏覽器開始計時器,0ms 後,() => {console.log(2)}
會被放到 callback queue 中等待執行將
setTimeout()
從 call stack 最上方移除console.log(3)
被 push 進 call stack 的最上方印出
3
將
console.log(3)
從 call stack 最上方移除setTimeout()
進入 call stack 最上方,讓瀏覽器開始計時器,0ms 後,() => {console.log(4)}
會被放到 callback queue 中等待執行console.log(5)
被 push 進 call stack 的最上方印出
5
將 main() 從 call stack 最上方移除
call stack 已清空,event loop 將 callback queue 中第一個 callback,也就是
() => {console.log(2)}
放到 call stack 最上方,執行之後發現這個 function 裡面還要呼叫 console.log(2),所以把 console.log 丟進去 call stack印出
2
將
console.log(2)
從 call stack 最上方移除call stack 已清空,event loop 將 callback queue 中第一個 callback,也就是
() => {console.log(4)}
放到 call stack 最上方,執行之後發現這個 function 裡面還要呼叫 console.log(4),所以把 console.log 丟進去 call stack印出
4
將
console.log(4)
從 call stack 最上方移除call back 與 callback queue 清空,程式執行完畢
JavaScript 是單執行緒程式語言,只有一個 call stack,一次也只能執行一件事,JS 中等待執行的任務會被放入 call stack。
既然是單執行緒語言,究竟要如何達成非同步操作?回到以瀏覽器當作執行環境舉例:
setTimeout(cb, 5000)
簡單說,就是告訴瀏覽器「 5000 毫秒後幫我將 cb 放到 queue」,瀏覽器就會開另一個 thread 去計時,不會利用 main thread 做這件事。接著瀏覽器某個 thread 計時 5000 毫秒(5 秒)到了,將剛剛的 cb 放入 callback queue 等待執行。
如果 call stack 為空,event loop 就會將 callback queue 中的 cb 放入 call stack 中。
參考
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
執行步驟:
宣告變數 i,賦值 0,i < 5,進入迴圈
console.log("i: " + i)
,也就是console.log("i: 0")
被 push 進 call stack 的最上方印出
i: 0
將
console.log("i: 0")
從 call stack 最上方移除setTimeout()
進入 call stack 最上方,讓瀏覽器開始計時器,0 秒後,() => {console.log(i)}
會被放到 callback queue 中等待執行,setTimeout()
pop 出 call stacki++
,i = 1
,迴圈繼續console.log("i: " + i)
,也就是console.log("i: 1")
被 push 進 call stack 的最上方印出
i: 1
將
console.log("i: 1")
從 call stack 最上方移除setTimeout()
進入 call stack 最上方,讓瀏覽器開始計時器,1 秒後,() => {console.log(i)}
會被放到 callback queue 中等待執行,setTimeout()
pop 出 call stacki++
,i = 2
,迴圈繼續console.log("i: " + i)
,也就是console.log("i: 2")
被 push 進 call stack 的最上方印出
i: 2
將
console.log("i: 2")
從 call stack 最上方移除setTimeout()
進入 call stack 最上方,讓瀏覽器開始計時器,2 秒後,() => {console.log(i)}
會被放到 callback queue 中等待執行,setTimeout()
pop 出 call stacki++
,i = 3
,迴圈繼續console.log("i: " + i)
,也就是console.log("i: 3")
被 push 進 call stack 的最上方印出
i: 3
將
console.log("i: 3")
從 call stack 最上方移除setTimeout()
進入 call stack 最上方,讓瀏覽器開始計時器,3 秒後,() => {console.log(i)}
會被放到 callback queue 中等待執行,setTimeout()
pop 出 call stacki++
,i = 4
,迴圈繼續console.log("i: " + i)
,也就是console.log("i: 4")
被 push 進 call stack 的最上方印出
i: 4
將
console.log("i: 4")
從 call stack 最上方移除setTimeout()
進入 call stack 最上方,讓瀏覽器開始計時器,4 秒後,() => {console.log(i)}
會被放到 callback queue 中等待執行,setTimeout()
pop 出 call stacki++
,i = 5
,迴圈結束將 main() 從 call stack 最上方移除
call stack 已清空,event loop 將 callback queue 中第一個 callback,
() => {console.log(i)}
,此時i = 5
,也就是() => {console.log(5)}
放到 call stack 最上方,執行之後發現這個 function 裡面還要呼叫 console.log(5),所以把 console.log 丟進去 call stack印出
5
將
console.log(5)
從 call stack 最上方移除call stack 已清空,event loop 將 callback queue 中第一個 callback,
() => {console.log(i)}
,此時i = 5
,也就是() => {console.log(5)}
放到 call stack 最上方,執行之後發現這個 function 裡面還要呼叫 console.log(5),所以把 console.log 丟進去 call stack印出
5
將
console.log(5)
從 call stack 最上方移除call stack 已清空,event loop 將 callback queue 中第一個 callback,
() => {console.log(i)}
,此時i = 5
,也就是() => {console.log(5)}
放到 call stack 最上方,執行之後發現這個 function 裡面還要呼叫 console.log(5),所以把 console.log 丟進去 call stack印出
5
將
console.log(5)
從 call stack 最上方移除call stack 已清空,event loop 將 callback queue 中第一個 callback,
() => {console.log(i)}
,此時i = 5
,也就是() => {console.log(5)}
放到 call stack 最上方,執行之後發現這個 function 裡面還要呼叫 console.log(5),所以把 console.log 丟進去 call stack印出
5
將
console.log(5)
從 call stack 最上方移除call stack 已清空,event loop 將 callback queue 中第一個 callback,
() => {console.log(i)}
,此時i = 5
,也就是() => {console.log(5)}
放到 call stack 最上方,執行之後發現這個 function 裡面還要呼叫 console.log(5),所以把 console.log 丟進去 call stack印出
5
將
console.log(5)
從 call stack 最上方移除call back 與 callback queue 清空,程式執行完畢
原始程式碼:
for (var i = 0; i < 5; i++) {
console.log("i: " + i);
setTimeout(() => {
console.log(i);
}, i * 1000);
}
因為 var
是 function scope variable,就是以 function 為作用域(換句話說,因為並沒有在 function 裡面宣告,所以 i 變成全域變數,任何地方都可以存取到它),因此可以看做:
var i;
for (i = 0; i < 5; i++) {
console.log("i: " + i);
setTimeout(() => {
console.log(i);
}, i * 1000);
}
當迴圈結束時,i 已經是 5 了,因此最後才會印出 5
setTimeout()
只能保證幾毫秒後即將會執行,但不能保證幾毫秒後會立即執行,因為他有可能還在 callback queue 中排隊,等待 call stack 清空,才輪得到 callback queue 中的 cb
參考
- 編譯階段時會做 hoisting,也就是提升宣告,但賦值不會提升
- 編譯階段會建立 Execution Context(下面簡稱 EC)
進入 EC 時會照順序做三件事:
找參數 -> 找 function -> 找變數找到傳進目前 function 的參數並放進 Variable Object(以下簡稱 VO),沒有傳值的話就是
undefined
找到目前 function 裡的 function 宣告放進 VO,如已有同名的,直接覆蓋掉
- 找到目前 function 中的變數宣告並放進 VO,如已有同名的,就不做事
原始程式碼:
var a = 1;
function fn() {
console.log(a); // undefined
var a = 5;
console.log(a); // 5
a++;
var a;
fn2();
console.log(a); // 20
function fn2() {
console.log(a); // 6
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
我們可以模擬 JS 引擎跑一次:
編譯階段:
global EC
global VO {
fn: function
a: undefined
}
執行階段:
global EC
global VO {
fn: function
a: 1
}
編譯階段:
fn EC
fn AO {
fn2: function
a: undefined
}
global EC
global VO {
fn: function
a: 1
}
執行階段:
fn EC
fn AO {
fn2: function
(console.log(undefined))
a: 5
(console.log(5))
}
global EC
global VO {
fn: function
a: 1
}
編譯階段:
fn2 EC
fn2 AO {
}
fn EC
fn AO {
fn2: function
a: 5
}
global EC
global VO {
fn: function
a: 1
}
執行階段:
fn2 EC
fn2 VO {
(因為這層沒有,就往上一層找,console.log(6))
}
fn EC
fn VO {
fn2: function
a: 20
(console.log(20))
}
global EC
global VO {
fn: function
a: 1
console.log(1)
b: 100
}
fn()內容執行結束,剩下:
編譯階段:
global VO {
a: 1
b: 100
}
執行階段:
global VO {
a: 10
(console.log(10))
b: 100
console.log(100)
}
程式結束
參考
原始程式碼:
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(); // ??
obj2.hello(); // ??
hello(); // ??
可以轉化成(主要是下面不一樣):
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().call(obj.inner); // ??
obj2.hello().call(obj2) => obj.inner.hello.call(obj.inner); // ??
hello().call(undefined); // ??
輸出:
2
2
undefined
脫離了物件導向的
this
就沒什麼意義,因為也沒有 instance 可以指向,換句話說,物件導向下的this
就是 instance 本身脫離物件導向,沒什麼意義的情況下,因為是一般模式,想知道
this
,會輸出undefined
,如果是嚴格模式 (use strict
),在瀏覽器上就會是window
,在 node.js 上會是global
this
只跟你在哪裡呼叫有關,跟你的程式碼位置、作用域都無關call() 傳入的任何參數都可以被當作
this
輸出,利用這一點,我們可以使用一點技巧幫我們知道目前的this
的值是什麼:ex :
obj.inner.hello() -> obj.inner.hello().call(obj.inner)
所以
this
的值就是obj.inner
,this.value
就是obj.inner.value
,印出2
參考
物件導向概念 (Object Oriented Programming) 名詞整理
基本概念:
類別(Class)
以現實生活比喻的話,就是藍圖、設計圖,接下來都會以汽車為例。
汽車設計藍圖上會有汽車的資訊跟操作方法(說明書),比如廠牌、規格、馬力、車名還有操控方法跟取得汽車資訊等。
以目前來說,定義抽象的特點,將上面轉換一下就是:
- 屬性(Field)-> 廠牌、規格、馬力、車名
- 方法(Method)-> 操控方法跟取得汽車資訊
class Car {
setName(name) {
this.name = name;
}
getInformation() {
return "廠牌" + "型號" + "馬力";
}
}
物件(Object)與實體(Instance)
有了上面的藍圖,我們就可以來創造實例,就是可以真的做很多台觸摸得到、可以開的車,而每一台車的資料都是獨立的,互不影響,但都同樣以這個藍圖為基底做實體化。
const Volksvagen = new Car('Volksvagen');
const Toyota = new Car('Toyota');
特性:
封裝(Encapsulation)
開車的人一般來說不必太了解汽車的內部構造,只要知道汽車長怎樣、會開就好。
這句話可以理解成,隱藏物件內部的資料、邏輯,只能透過物件提供的介面(interface)去取得內部屬性或方法,物件內部的細節資料或邏輯則隱藏起來。
如果不經過允許的窗口(物件提供的方法),便無從更動物件內部的資料。例如我們可以藉由 Toyota.getInformation()
去取得汽車資訊,但不必知道他怎麼取得資訊的。
繼承(Inheritance)
可是每台車我們都想要再做點變化,可能是汽缸加大、增加氮氣、增加幫浦等,那麼「子類別」就會繼承「父類別」,意思就是除了繼承原有的屬性與方法,也能增加自己獨有的屬性或方法。
比如今天我要做一台阿斯拉:
class Car {
setName(name) {
this.name = name;
}
getInformation() {
return "車名";
}
}
class raceCar extends Car {
constructor(name) {
super(name);
this.getInformation();
}
addHP() {
console.log("已經加大 500 匹馬力!");
}
}
const myCar = new raceCar("Asurada");
一些自我問答:
類別不會包含物件(設計圖內不會包含實體汽車)
物件不會包含類別(實體物件在被 new 出來以前都是虛擬的,實體物件不該包含虛擬類別)
類別可以包含類別(設計圖中有車體的設計、方向盤的設計)
物件可以包含物件(實體汽車中可以包含實體方向盤)
物件可以當作資料傳遞(物件可透過編碼方式傳遞)
類別不能當作資料傳遞(物件導向中,類別屬於虛擬的,無法傳遞)
同上一條,兩台電腦間傳遞的資料為「類別」
物件不包含方法(method 會被包含在設計圖,也就是類別中)
參考: