iT邦幫忙

2021 iThome 鐵人賽

DAY 12
0
Modern Web

Javascript 從寫對到寫好系列 第 12

Day 12 - OOP 初探 (2) - Class

前言

昨天講完 Javascript OOP 兩個重要支柱,今天接著這個主題,來講講 class 吧!

Class(類別)

Class 可以想像成印章,每壓一下就蓋出一個印,每 new 一下就產生一個物件。

class 是 ECMAScript 6 引入的語法,但由於 Javascript 仍是基於原型(prototype-based)的語言,所以這個所謂的 class,其實也只是語法糖,Javascript 依然沒有真正的 "class",所以透過原型鏈(prototype chain) 來營造出繼承的效果。

Class 宣告方式

class 可以當作特別形式的函式,所以宣告也有分 class expressions 跟 class declarations,我個人比較喜歡後者,可以少寫一點XD

class expressions

const Person = class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
};

class declarations

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

上述都只是宣告,現在用 new 來真正將這些 class「實體化」:

const person = new Person('Joey', 20);

Class 語法介紹

基本上 class 內的語法,跟我們昨天用 function 在寫的時候其實非常像,只是有幾點需要注意:

constructor,用來建立和初始化一個類別的物件,裡面基本上就是在做初始化,且一個 class 裡面只能有一個。

而一般的 method 則是像下面這樣,用一般的非箭頭函式,但 function 關鍵字也可以省略:

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  sayHello() {
      return `Hello, ${this.name}~`;
  }
}

const person = new Person('Joey', 20);
console.log(person.sayHello());

執行結果

Hello, Joey~

class 有幾個分解動作,是 Javascript 背後幫我們完成的:

  • class 裡的 constructor() 抓出來,指定給 Person
  • class 裡的其他 method 指定給 Person.prototype

你會發現其實用 function 都做得到,只是用 class 會比較有在寫 OOP 的感覺,所以才說 class 是「特別形式」的 function。

extends 子類別

比如說我們有個 class 叫做 Animal

class Animal {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  speak() {
    console.log(this.name + ' 發出聲音!');
  }
}

Animal 是個比較大的範圍,動物(基本上)都會發出聲音,如果今天我們需要建立一個貓的類別,貓也是動物的一種,所以動物會有的屬性,貓都會有:

class Animal {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  speak() {
    console.log(this.name + ' 發出聲音!');
  }
}

class Cat {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  speak() {
    console.log(this.name + ' 發出聲音!');
  }
}

可以看到其實很大量的 code 在重複,有這種大類別包含小類別的狀況,就可以使用 extends 去繼承大類別:

class Animal {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  speak() {
    console.log(this.name + ' 發出聲音!');
  }
}

class Cat extends Animal {}

const cat = new Cat('Lulu', 5);
cat.speak();
console.log(cat.age);

執行結果

Lulu 發出聲音!
5

覆寫父類別的 method

但基本上不會只寫一行

class Cat extends Animal {}

因為如果子類別長得跟父類別一模一樣,那好像也沒什麼必要分出來,假如我們希望讓貓的聲音更有區別性,可以去覆寫父類別的 method:

class Animal {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  speak() {
    console.log(this.name + ' 發出聲音!');
  }
}

class Cat extends Animal {
  speak() {
    console.log(this.name + ' 喵~~~~');
  }
}

const cat = new Cat('Lulu', 5);
cat.speak();
console.log(cat.age);

執行結果

Lulu 喵~~~~!
5

super 呼叫父類別建構子

但你是否有注意到一點,Cat 類別裡面怎麼會沒有 constructor method?這樣也能跑嗎?

其實是可以的,因為如果用 extends 語法去繼承父類別,而又沒有給 constructor method 的時候,Javascript 會在背後偷偷幫你補上:

class Cat extends Animal {
  // 這一段是自動補上的 start
  constructor(...args) {
    super(...args);
  }
  // 這一段是自動補上的 end
  
  speak() {
    console.log(this.name + ' 喵~~~~');
  }
}

這個 super 是只能夠用在子類別的語法,意思是呼叫父類別的建構子。constructor 裡面務必要先執行 superthis 才會有東西,再開始使用 this.namethis.age 之類的屬性,不然像這樣死掉哦:

Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

類別方法 v.s. 實體物件方法

發現 class 講完還有一點時間,我們再來看看,同樣是 method,寫在類別上,跟寫在實體化的物件上,有什麼樣的差別呢?

假如我們一樣是電商系統,要判斷 user 的錢包有沒有辦法負擔商品價格,所以我們有個 checkPriceAffordable 的 method。

方案A,如果寫在類別上是這樣:

class User {
    constructor(name, money) {
        this.name = name;
        this.money = money;
    }
    
    checkPriceAffordable(price) {
        return this.money >= price;
    }
}

const user = new User('Joey', 2000);

if (user.checkPriceAffordable(1000)) {
    console.log('可以買! 買起來!!!!');
}

執行結果

可以買! 買起來!!!!

方案 B,寫在實體化出來的物件裡面:

class User {
    constructor(name, money) {
        this.name = name;
        this.money = money;
        this.checkPriceAffordable = function(price) {
            return this.money >= price;
        }
    }
}

const user = new User('Joey', 2000);

if (user.checkPriceAffordable(1000)) {
    console.log('可以買! 買起來!!!!');
}

執行結果

可以買! 買起來!!!!

差別其實就在於

  • 方案A(存在類別裡)從頭到尾都只有一個 checkPriceAffordable
  • 方案B(存在實體化物件裡)則是new 一次就複製一個

效能落差?

當我們呼叫 user.checkPriceAffordable() 的時候,Javascript 是怎麼找到 checkPriceAffordable 這個 method 呢? 

查找的順序就是由內而外,沿著原型鏈往上找,先看 user 物件本人有沒有這個 method,如果沒有就順著 __proto__ 往上看 User 這個類別有沒有。

所以理論上好像會是存在實體化物件上,呼叫的效率最高。

但事實上是,Javascript 當然也知道這一點,因此有針對原型鏈查找的效能最佳化,其實花費的時間不會差太多,但可以肯定的卻是,存在實體化物件中,每複製一份就多花一份記憶體來存。

因此原則上還是把共用的 method 直接寫在類別或原型上即可。

結語

Javascript 雖然常被說很鬆散,過度自由,但也是因為如此,要寫 FP 要寫 OOP,都可以自由選擇,class 雖然也是透過 prototype 做出來的語法糖,少了像是 privatepublic 這種方便的語法,但有總比沒有好,讓這個語言充滿了非常多可能性!

在喧囂嘈雜的社會裡
或許你我都是
寂靜的克隆體

參考資料

Class MDN
深入淺出 JavaScript ES6 Class


上一篇
Day 11 - OOP 初探 (1) - Closures 與繼承鏈
下一篇
Day 13 - OOP 初探 (3) - 實戰地圖遊戲
系列文
Javascript 從寫對到寫好30

1 則留言

0
TD
iT邦新手 4 級 ‧ 2021-09-28 10:02:05

少了像是 private、public 這種方便的語法

來寫 TypeScript 吧~ (逃)

ycchiuuuu iT邦新手 5 級 ‧ 2021-09-29 13:08:52 檢舉

TypeScript 真的是時代所趨啊!這個時代的年輕人要學的東西愈來愈多了XD

我要留言

立即登入留言