iT邦幫忙

2024 iThome 鐵人賽

DAY 7
0
JavaScript

Don't make JavaScript Just Surpise系列 第 7

JavaScript 的類別(Class)與物件導向(OO)

  • 分享至 

  • xImage
  •  

物件導向(Object-oriented, OO)想必是個大家耳熟能詳的詞語。
物件導向指的是編寫程式時以物件為主題,注重物件上的屬性(資料)和方法(行為),雖然可能與傳統的物件導向程式如 C++ 有所差異,但 JS 毫無疑問是個物件導向的語言。
這篇會著重在討論物件導向的特性與 JS 如何針對這些特性進行實作,從中就能看到上述說的「差異」。
讓我們開始吧。

類別(Class)

物件導向的核心概念:物件,在許多程式語言中是以類別的形式呈現。
如 C# 中,類別是物件的結構定義,物件則特指類別產生的實體(instance)。

public class Human {
    public string Name { get; set; }
    
    public void Hello() {
        Console.WriteLine($"{Name} says Hi!");
    }
}//類別
Human friend1 = new Human();//物件
friend1.Name = "Ken";
myDog.Hello(); 

在 JS 中,實際上不存在從結構實體化的概念,非原始型別的,都能看作是一個物件(如 Day6 所解釋)。
比起實體化,更像是關聯了(位址指向上)定義的類別物件與被new新建立物件
而 類別(Class) 這個關鍵字一直到 ES6 才被引入。

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

	hello() {
		console.log(`${this.name} says Hello`);
	}
}

物件導向中類別的特性包括了幾個點:實體化(instantiation),繼承(inheritance),多型(polymorphism)。

實體化(instantiation)

類別本身代表一個結構,一種設計,描述資料和行為。
如同上面的例子,Human 是一個類別,描述了這個類別具有 name 這個屬性和 hello 這個方法,但沒有被實體化的 Human 本身只是一種結構,規範,一般我們並不會對他進行直接操作,而是需要先進行實體化。

...
let friend1 = new Human('Ken');
friend1.hello();//"Ken says Hello"

這邊的 friend1 形成了一個類別是 Human,能夠被操作的物件。
就像我們可以讓他打招呼一樣,他會以自身上的屬性來執行對應的方法。
new 的關鍵字會觸發建構這個行為。

建構式(Constructor)

在 JS 中,建構式通過 new 被調用,ES 6 之前,尚無 Class 關鍵字的時候可能會這樣寫:

function Human(name){
    this.name = name
    this.hello = function(){console.log(this.name + ' says Hello')};
}

new 背後的的行為就是初始化一個新的物件,並根據傳入的參數為物件作賦職等對應操作。
從 ES 6 開始,Class 關鍵字被加入後,建構式就有專屬的關鍵字 constructor

class Human {
	constructor( name ) {
		this.name = name;
	}
}
Human.prototype.hello = function(){
    console.log(`${this.name} says Hello`);
}

上兩例有著相近的行為,再透過這樣的方式宣告後,無論是哪個版本,根據下面的語法都能印出相同的結果。

var friend1 = new Human('Ken');
friend1.hello();

那如果忘記加 new 會發生什麼事?

var friend2 = Human('Ryu');
console.log(friend2);//undefined
friend2.hello();//Uncaught TypeError: Cannot read properties of undefined (reading 'hello')"

可以看到實際上他變為一個 undefined,為什麼?因為建構式的定義下,new 關鍵字必定會回傳一個被建構的物件,而向這個例子,類別本身並沒有實作 return,正常也不需要實作。
new 的機制下,return 除了物件之外的類別都會失效(會被忽略而依然回傳新建實體),回傳物件則會傳物件而不是新建的實體。

function Human(name){
    this.name = name
    return "This is no expected";
}
Human.prototype.hello = function(){
    console.log(`${this.name} says Hello`);
}

var friend1 = new Human('Ken');
friend1.hello();//無視 "This...",依然回傳"Ken says Hello"

var friend2 = Human('Ryu');
console.log(friend2);//"This is no expected",沒有成功創建物件,而是當作一個函式使用去接回傳值

這就導致 Human 除去類別的涵義之後,只是一個純粹無回傳的函式,那一個無回傳的函式的回傳值是什麼,就是 undefined
若沒有建構式的情況下,會回傳一個預設建構式的空物件。

class EmptyObj{};
let emp = new EmptyObj();
console.log(emp);//{}

繼承(inheritance)

繼承的目的是基於某個類型上的原型,進一步實踐更特定範圍的定義。
就像集合論理的子集,於一個特定範圍中再畫出更小的一個範圍。
實際上在命名上也是用這個概念,被繼承的稱作父類別,繼承父類別的稱作子類別。

...
class Classmate extends Human {
    constructor(name, studentId) {
        super(name); //使用 Human 的建構方法建構
        this.studentId = studentId;
    }

    showId() {
        console.log(`My student Id is ${this.studentId}`);
    }
}
let friend2 = new Classmate('Ryu','10000');
friend2.hello();//"Ryu says Hello"
friend2.showId();//"My student Id is 10000"

如同上面這個例子,Human 定義了人這個類別,而 Classmate 基於 Human 這個類型上,進一步定義了同學這樣的類別,保有 hello 方法,並實作專屬於 Classmate 的 showId 方法。

super 關鍵字

上面繼承的例子可以看到 super 這個關鍵字,這個關鍵字專用於繼承的情況,且必須定義在建構式中任一次 this 被呼叫之前。
super(name) 的行為實際上等同呼叫 Human(name),父類別的建構式,只是 this 指向當前新產生的物件。
因此可以看到在呼叫 .hello() 的時候,能夠正確以傳入的 name 展現父類別的行為。

...
class Classmate extends Human {
    constructor(name, studentId) {
        this.studentId = studentId;
        super(name); //沒有放在使用 this 之前會報錯
    }
    showId() {
        console.log(`My student Id is ${this.studentId}`);
    }
}
//Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor"

這個關鍵字也能拿來呼叫父類別的方法,如果是用作呼叫函式而非建構屬性,則沒有需要放在 this 之前的限制。

...
    showId() {
        console.log(`My student Id is ${this.studentId}`);
        super.hello();
    }
//"My student Id is 10000"
//"Ryu says Hello"
...

如果以上面這段改寫 showId(),則會如註解中首先印出這個類的方法內容(因為語句順序上放前面),再印出父類別的 hello 方法。

多型(polymorphism)

多型在物件導向的定義是,針對繼承鏈中的不同類別,同一個方法可以有不同的實作。
在 ES 6 出來之前,實作這件事需要大量的精力來處理,如果有興趣確認詳細內容,建議閱讀 YDKJS 中的對應章節。這邊會專注在有 ES 6 之後的語法及其能力、限制。

...
class Japanese extends Human{
    constructor(name){
        super(name);
    }
    hello(){
        super.hello();
        console.log(`Konichiwa! I am ${this.name}.`);
    }
}
let friend3 = new Japanese('Gouki');
friend3.hello();

上面的方法額外定義了另一種不同的打招呼方式,且並未覆寫本身 Human 的打招呼定義,將新的打招呼實作僅限於 Japanese 這個類別。
即使子類的 hello 覆寫了父類的同名方法,仍能透過 super 關鍵字呼叫父類方法,再添上自己的實作。

JS 中物件導向與其他物件導向語言的差異(以C# 為例)

常有人會說 JS 的物件導向和其他物件導向語言實作上是有差異的,我們以 C# 為例來做比較,結合上面段落,看看到底是差在哪裡。

  1. 實體化
    JS 如上面說的,並沒有結構藍圖的概念,所有東西都是實際的物件,而 C# 中的類別就真的只是藍圖,必須實體化後才能夠操作。
  2. 實際上 class 是語法糖(syntax sugar)
    承 1., class 在 JS 中只是一種語法糖,意指並未實作新的底層邏輯,而是將既有機制以某種方式囊括後實作。就如 class 其實只是在 ES6 被引入的語法,透過背後的包裝,讓我們能夠相對簡單的來處理類型的行為。
  3. 原型鏈(Prototype Chain)與繼承
    在 C# 中的物件繼承,來自於 Class 層層繼承, Class 在 C# 中如 1. 所講,他是一個真的存在,用於結構(本篇提到的結構都是概念上,並非C# 中的 struct 關鍵字,特別說一下避免誤會)定義的語法。那 JS 呢?實際上是 object 與另一個 object 鏈結,技術用語上來說,我們會說 JS 是種 基於原型(prototype-based)的語言,透過原型鏈(Prototype Chain)來展現繼承概念。
    這邊突然拋出了兩個新名詞:原型?原型鏈?沒錯,讓我們在明天的篇章裡再來好好討論原型是怎麼一回事。

上一篇
物件(object)與複製行為
下一篇
原型與相關關鍵字([[Prototype]],__proto__,.prototype)
系列文
Don't make JavaScript Just Surpise31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言