物件導向(Object-oriented, OO)想必是個大家耳熟能詳的詞語。
物件導向指的是編寫程式時以物件為主題,注重物件上的屬性(資料)和方法(行為),雖然可能與傳統的物件導向程式如 C++ 有所差異,但 JS 毫無疑問是個物件導向的語言。
這篇會著重在討論物件導向的特性與 JS 如何針對這些特性進行實作,從中就能看到上述說的「差異」。
讓我們開始吧。
物件導向的核心概念:物件,在許多程式語言中是以類別的形式呈現。
如 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)。
類別本身代表一個結構,一種設計,描述資料和行為。
如同上面的例子,Human 是一個類別,描述了這個類別具有 name 這個屬性和 hello 這個方法,但沒有被實體化的 Human 本身只是一種結構,規範,一般我們並不會對他進行直接操作,而是需要先進行實體化。
...
let friend1 = new Human('Ken');
friend1.hello();//"Ken says Hello"
這邊的 friend1 形成了一個類別是 Human,能夠被操作的物件。
就像我們可以讓他打招呼一樣,他會以自身上的屬性來執行對應的方法。
new
的關鍵字會觸發建構這個行為。
在 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);//{}
繼承的目的是基於某個類型上的原型,進一步實踐更特定範圍的定義。
就像集合論理的子集,於一個特定範圍中再畫出更小的一個範圍。
實際上在命名上也是用這個概念,被繼承的稱作父類別,繼承父類別的稱作子類別。
...
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
這個關鍵字,這個關鍵字專用於繼承的情況,且必須定義在建構式中任一次 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
方法。
多型在物件導向的定義是,針對繼承鏈中的不同類別,同一個方法可以有不同的實作。
在 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# 為例來做比較,結合上面段落,看看到底是差在哪裡。
class
是語法糖(syntax sugar)class
在 JS 中只是一種語法糖,意指並未實作新的底層邏輯,而是將既有機制以某種方式囊括後實作。就如 class
其實只是在 ES6 被引入的語法,透過背後的包裝,讓我們能夠相對簡單的來處理類型的行為。