iT邦幫忙

2022 iThome 鐵人賽

DAY 14
2

前言

這篇將會介紹 ES6 推出了新的 Class 語法,在背後的運作還是以原型為基礎 (prototype based) 的繼承。

2022/9/25 更新

但要注意 ES6 Class 它並非是完全的語法糖,底下留言區有良葛格可以參考,我這裡也補充一些內容給讀者:

JS classes are not “just syntactic sugar” 這篇文章的作者透過一些特性的舉例來說明為什麼不完全是語法糖。

對於開發者來說,可以使用更簡潔簡單的語法去實作繼承,熟悉以類別為基礎的物件導向程式語言的開發者也能更容易上手。


從範例認識 Class 和相關關鍵字

首先,我們來看個範例,然後一一解說範例中的一些重點。

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

  getName() {
    console.log("此為父類別的方法");
    console.log(this.name);
  }
}

class Dog extends Animal{
  constructor(name, age) {
    super(name);
    this.classis = 'Mammalia'; // classic 為拉丁文的綱,Mammalia 則為哺乳綱、哺乳動物的意思
    this.age = age;
  }

  getName() {
    console.log("此為子類別的方法", this.name);
  }
  
  static seeClassis(pet) {
    console.log(pet.classis);
  }
  
  set petAge(value) {
    this.age = value;
  }
  
  get petAge() {
    return this.age;
  }
}

const myPet = new Dog('Lucky', 5);

myPet.getName(); // "此為子類別的方法" Lucky

Dog.seeClassis(myPet); // "Mammalia"

console.log(myPet.petAge); // 5
myPet.petAge = 6;
console.log(myPet.petAge); // 6

const herPet = new Dog('Buddy', 4);
console.log(myPet.__proto__ === herPet.__proto__); // true

看完程式碼後,以下將會擷取範例程式碼片段一一說明。

Class & constructor

範例中,透過了 Class 建立一個類別,而 constructor 建構子是個隨著 class 一同建立並初始化物件的特殊方法,一個類別只能有一個建構子。

class Animal {
  constructor(name) {...}
}

extends & super

extends 能用來繼承類別,要注意的是子類別的方法會蓋掉父類別的同名方法。

而當子類別自身也需要透過 constructor 建立 Properties 時,就需要使用 super,super 會呼叫父類別的建構式,並指定要提供給父類別的值。

super 功能:

  1. 提供一個類別呼叫其父類別的函數(super call)
  2. 建構子可以用 super 呼叫父類別的建構子。

第一點例如可以在子類別 Dog 內的 getName 函式加上 super.getName();,就會執行父類別的 getName 函式。

class Dog extends Animal {
  constructor(name, age) {
    super(name);
    this.classis = 'Mammalia'; // classic 為拉丁文的綱,Mammalia 則為哺乳綱、哺乳動物的意思
    this.age = age;
  }
	
	getName() {
    super.getName(); // 印出 "此為父類別的方法" Lucky
    console.log("此為子類別的方法", this.name);
  }
}

原型方法(prototype method)

例如範例的 getName(),讓類別的實體(instance)使用的方法。

getName() {
  console.log("此為子類別的方法", this.name);
}

靜態方法(Static method)

透過 static 這個關鍵字來建立 static method,只存在 class 中,不能被實體所使用,經常被用來建立給類別使用的工具函數。

static seeClassis(pet) {
  console.log(pet.classis);
}

// 使用
Dog.seeClassis(myPet); // "Mammalia"

Setter 和 Getter

用來取得和修改物件屬性的方法,setter 要有傳入的變數,而 getter 則沒有,使用它們可以封裝內部邏輯。

set petAge(value) {
  this.age = value;
}
  
get petAge() {
  return this.age;
}

new 關鍵字

用來建立物件。


新語法 - # 建立私有變數、私有方法

在這個 "#" 語法推出之前,類別內並沒有像其他程式語言中有:

  • 私有(private)
  • 保護(protected)
  • 公有(public)

這幾種存取控制的修飾子,基本上所有的類別中的成員都是公開的。

三種修飾子差別:

  • public 修飾的屬性或方法是公有的,可以在任何地方被訪問到,預設所有的屬性和方法都是 public 的
  • private 修飾的屬性或方法是私有的,不能在宣告它的類別的外部訪問
  • protected 修飾的屬性或方法是受保護的,它和 private 類似,區別是它在子類別中也是允許被訪問的

而在 ES2020 出現的新語法,加上 "#" 就能將一個變數或是方法變成私有,私有屬性必須在建立類別時事先宣告。

範例:

class Rectangle {
  #height = 0;
  #width;
  constructor(height, width) {
    this.#height = height;
    this.#width = width;
  }
}

避免寫出全域的函式

直接舉個例子,也許讀者曾經看過透過 .prototype 的方式去建立一個函式:

Array.prototype.doSomething = function(){
   ...
}

但這樣會有個問題,如果你有用到其他的函式庫並且剛好有同名的函式,就可能會有功能被影響的風險,讀者可以參考 Stack Overflow - adding custom functions into Array.prototype 的討論,除了文章提到的 Object.defineProperty() 能建立函式外,也可以運用我們這篇學到的 Class。

Array.prototype.myMap = function(fn, thisArg) {
  const result = [];
  for (let i = 0; i < this.length; i++) {
	result.push(fn.call(thisArg || null), this[i], i, this));
  }
  return result;
};

// 透過建立子類別,並在子類別新增函式
class ExtendedArray extends Array {
  myMap(fn, thisArg) {
    const result = [];
    for (let i = 0; i < this.length; i++) {
	  result.push(fn.call(thisArg || null), this[i], i, this));
    }
    return result;
  };
}

ES6 的 Class 語法有什麼優點?

在介紹那麼多的語法後,我們來思考一些問題,Class 語法帶給了我們什麼幫助呢?

第一就是使用了簡單、閱讀性高、較容易維護的語法去實作物件導向的繼承,也蠻容易看出繼承間的關係,而且其他語言的工程師也相對容易上手。

另外也比 ES5 語法多了更多修飾子(ex: static、#)或是功能可以運用。


延伸學習-TypeScript

在 TypeScript 中,Class 還有更多相關的語法提供給開發者使用,像是有更多的修飾子,public、private、protected 和 readonly,以及 abstract class、implements 等。

因為這個鐵人賽的主題著重在 JS,所以對這些 TS 語法有興趣的讀者可以另外參考 TypeScript 官網對於 Classes 的文件


參考資料 & 推薦閱讀

Classes - JavaScript - MDN Web Docs

[JS] JavaScript 類別(Class)

[教學] 深入淺出 JavaScript ES6 Class (類別)


上一篇
Day13-圖解原型繼承與原型鏈
下一篇
Day15-Object.create() 介紹
系列文
強化 JavaScript 之 - 程式語感是可以磨練成就的30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

1
良葛格
iT邦新手 2 級 ‧ 2022-09-25 11:10:06

class 最後是基於原型來實現沒有錯,不過也不完全是語法糖。

例如,類別語法的繼承,能夠繼承標準 API,而且內部實作特性以及特殊行為也會被繼承,例如,可以繼承 Array,子型態實例的 length 行為,能隨著元素數量自動調整等,內部實作特性以及特殊行為也會繼承。

例如,Array.isArray 的判定依據,就是陣列內部實作特性 [[Class]] 的值 'Array',在以前,物件的原型若被修改為 Array.prototype,雖然可以騙過 instanceof,然而不會被 Array.isArray 判定為 true,然而透過 class 來繼承 Array,引擎會繼承內部特性 [[Class]],Array 的子類實例可以被 Array.isArray 判定為 true

harry xie iT邦研究生 1 級 ‧ 2022-09-25 22:42:24 檢舉

這裡謝謝良葛格的改正,那看來自己過去學的觀念和網路上很多文章都寫得不夠正確,我這裡也有閱讀了一些文章,會再更新在文章上!

JS classes are not “just syntactic sugar”

Are ES6 classes just syntactic sugar for the prototypal pattern in Javascript?

我要留言

立即登入留言