回頭來看一些比較基本的東西:JavaScript 核心與物件導向


Posted by 小小碼農 on 2021-07-24

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
  1. 全域環境程式(這裡稱作 main())進入 call stack

  2. console.log(1) 被 push 進 call stack 的最上方

  3. 印出 1

  4. console.log(1) 從 call stack 最上方移除

  5. setTimeout() 進入 call stack 最上方,讓瀏覽器開始計時器,0ms 後,() => {console.log(2)} 會被放到 callback queue 中等待執行

  6. setTimeout() 從 call stack 最上方移除

  7. console.log(3) 被 push 進 call stack 的最上方

  8. 印出 3

  9. console.log(3) 從 call stack 最上方移除

  10. setTimeout() 進入 call stack 最上方,讓瀏覽器開始計時器,0ms 後,() => {console.log(4)} 會被放到 callback queue 中等待執行

  11. console.log(5) 被 push 進 call stack 的最上方

  12. 印出 5

  13. 將 main() 從 call stack 最上方移除

  14. call stack 已清空,event loop 將 callback queue 中第一個 callback,也就是 () => {console.log(2)} 放到 call stack 最上方,執行之後發現這個 function 裡面還要呼叫 console.log(2),所以把 console.log 丟進去 call stack

  15. 印出 2

  16. console.log(2) 從 call stack 最上方移除

  17. call stack 已清空,event loop 將 callback queue 中第一個 callback,也就是 () => {console.log(4)} 放到 call stack 最上方,執行之後發現這個 function 裡面還要呼叫 console.log(4),所以把 console.log 丟進去 call stack

  18. 印出 4

  19. console.log(4) 從 call stack 最上方移除

  20. 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

執行步驟:

  1. 宣告變數 i,賦值 0,i < 5,進入迴圈

  2. console.log("i: " + i) ,也就是 console.log("i: 0") 被 push 進 call stack 的最上方

  3. 印出 i: 0

  4. console.log("i: 0") 從 call stack 最上方移除

  5. setTimeout() 進入 call stack 最上方,讓瀏覽器開始計時器,0 秒後,() => {console.log(i)} 會被放到 callback queue 中等待執行,setTimeout() pop 出 call stack

  6. i++i = 1,迴圈繼續

  7. console.log("i: " + i) ,也就是 console.log("i: 1") 被 push 進 call stack 的最上方

  8. 印出 i: 1

  9. console.log("i: 1") 從 call stack 最上方移除

  10. setTimeout() 進入 call stack 最上方,讓瀏覽器開始計時器,1 秒後,() => {console.log(i)} 會被放到 callback queue 中等待執行,setTimeout() pop 出 call stack

  11. i++i = 2,迴圈繼續

  12. console.log("i: " + i) ,也就是 console.log("i: 2") 被 push 進 call stack 的最上方

  13. 印出 i: 2

  14. console.log("i: 2") 從 call stack 最上方移除

  15. setTimeout() 進入 call stack 最上方,讓瀏覽器開始計時器,2 秒後,() => {console.log(i)} 會被放到 callback queue 中等待執行,setTimeout() pop 出 call stack

  16. i++i = 3,迴圈繼續

  17. console.log("i: " + i) ,也就是 console.log("i: 3") 被 push 進 call stack 的最上方

  18. 印出 i: 3

  19. console.log("i: 3") 從 call stack 最上方移除

  20. setTimeout() 進入 call stack 最上方,讓瀏覽器開始計時器,3 秒後,() => {console.log(i)} 會被放到 callback queue 中等待執行,setTimeout() pop 出 call stack

  21. i++i = 4,迴圈繼續

  22. console.log("i: " + i) ,也就是console.log("i: 4") 被 push 進 call stack 的最上方

  23. 印出 i: 4

  24. console.log("i: 4") 從 call stack 最上方移除

  25. setTimeout() 進入 call stack 最上方,讓瀏覽器開始計時器,4 秒後,() => {console.log(i)} 會被放到 callback queue 中等待執行,setTimeout() pop 出 call stack

  26. i++i = 5,迴圈結束

  27. 將 main() 從 call stack 最上方移除

  28. 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

  29. 印出 5

  30. console.log(5) 從 call stack 最上方移除

  31. 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

  32. 印出 5

  33. console.log(5) 從 call stack 最上方移除

  34. 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

  35. 印出 5

  36. console.log(5) 從 call stack 最上方移除

  37. 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

  38. 印出 5

  39. console.log(5) 從 call stack 最上方移除

  40. 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

  41. 印出 5

  42. console.log(5) 從 call stack 最上方移除

  43. 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 -> 找變數

    1. 找到傳進目前 function 的參數並放進 Variable Object(以下簡稱 VO),沒有傳值的話就是 undefined

    2. 找到目前 function 裡的 function 宣告放進 VO,如已有同名的,直接覆蓋掉

    3. 找到目前 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.innerthis.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");

一些自我問答:

  1. 類別不會包含物件(設計圖內不會包含實體汽車)

  2. 物件不會包含類別(實體物件在被 new 出來以前都是虛擬的,實體物件不該包含虛擬類別)

  3. 類別可以包含類別(設計圖中有車體的設計、方向盤的設計)

  4. 物件可以包含物件(實體汽車中可以包含實體方向盤)

  5. 物件可以當作資料傳遞(物件可透過編碼方式傳遞)

  6. 類別不能當作資料傳遞(物件導向中,類別屬於虛擬的,無法傳遞)

  7. 同上一條,兩台電腦間傳遞的資料為「類別」

  8. 物件不包含方法(method 會被包含在設計圖,也就是類別中)

參考:


#Object-Oriented-Programming







Related Posts

D14_ ALG101

D14_ ALG101

使用 Golang 打造 Discord 機器人 (二)

使用 Golang 打造 Discord 機器人 (二)

C++ : namespace (1)

C++ : namespace (1)


Comments