JS: OOP - Inheritance

接續上一篇的物件導向介紹的OOP基本介紹,這篇說明Object.create、contstructor function和ES6 class語法如何建立prototypes(原型)之間的繼承關係,形成所謂的原型鏈(prototype chain)


Object.create

複習一下,Object.create方法暗地裡其實就是用來鏈結某東西與prototype之間的關係,例如前一篇是用 const jeck = Object.create(UserProto),讓物件jeck可以繼承UserProto的prototype並存取其方法。因此,同樣也能透過Object.create建立prototypes之間的繼承關係。

延續前一篇假設已經有一個父類別的prototype為 UserProto

const AdminProto = Object.create(UserProto);

AdminProto.init = function(account, password, key){
    UserProto.init.call(this, account, password);
    this.key = key;
}

const mike = Object.create(AdminProto);

範例中使用Object.create建立prototypes之間的繼承關係,AdminProto的父層prototype即為UserProto,這點可以用 mike.__proto__.__proto__ === UserProto 來檢查。

此外,為了繼承(委託)父層prototype已有的 init(account, password) 函式,範例中對子層prototype,也就是AdminProto建立了一個同名但部分參數不同的函式 init(account, password, key),並在函式裡用 call 方法去綁定父層prototype的init函式。

到這裡可能會想說為何不可以直接呼叫 UserProto.init 函式?原因在於如果用一般的函式呼叫方法,按照this的規則,一般函式的this是undefined,也就不能讓init函式做到:

this.account = account;
this.password = password;

因為this是undefined。

子層prototype也可以擁有自己的方法,這點的寫法就跟父層prototype寫函式的方式一樣,就不贅述了。

Constructor Function

要如何讓constructor functions們的prototypes之間有繼承關係?

看到這個問題可以回想剛才Object.create案例也有類似的狀況。前面案例用Object.create就可以讓UserProto和AdminProto的prototypes之間有繼承關係,所以同樣的手法也可用在constructor functions的prototypes們身上。

先來建立子層物件的constructor function,寫法類似於Object.create案例裡AdminProto的init函式:

const Admin = function(account, password, key){
    User.prototype.call(this, account, password);
    this.key = key;
}

Admin.prototype = Object.create(User.prototype);

conse mike = new Admin("mike456", 456, a007)

再複述一次,Object.create是用來建立prototypes之間的親子繼承關係,所以沒辦法直接寫成:

Admin.prototype = User.prototype

這句的意思是指Admin.prototype其實就等於User.prototype,兩個指向相同的prototype,並沒有建立兩個prototypes之間的繼承關係。

如果想檢查Admin這個constructor function的prototype是否指向正確的prototype,除了可以用

  • mike.__proto__ 來檢查物件的 __proto__ 是否指向Admin的prototype,或者
  • Admin.prototype.isPrototypeOf(mike) 直接確認Admin的prototype是否就是其物件mike的prototype,

也可以用 Admin.prototype.constructor 來看constructor是否指回Admin自己。

ES6 Class

相較於前面兩個比較複雜的繼承關係建立方式,ES6 Class的繼承就顯得比較親切許多(如果有寫過其他程式語言的話)

class Admin extends User {
    constructor(account, password, key){
        super(account, password);
        this.key = key;
    }
    // 可以寫Admin自己的prototype方法
}

ES6 class語法是很直覺地直接用 extends 關鍵字讓Admin繼承User,而constructor裡的 super 就是向JavaScript明示要呼叫父層User的constructor。

此外,如果Admin的constructor沒有要輸入account和password以外的屬性(key),其實也可以不必特別用 super(),因為 extends 這個關鍵字就會在背地裡自動呼叫 super(account, password),就能省去這道功夫。

父層和子層或者他們各自的prototypes都可以擁有自己的屬性,如果想要檢查是否為某個constructor的某個屬性或方法,可以用 instance.hasOwnProperty(/* property name */) 來確認。

另外,有關於物件導向還有幾個比較分散的小主題,像是靜態屬性、靜態方法、setter和getter,就統一彙整在最後一篇文章。


References The Complete JavaScript Course 2023: From Zero to Expert!