Javascript - 繼承(EcmaScript 6之前)

上一篇限量介紹了在 ES6 之前如何在 Javascript 建立類別Namespace,但是還沒講到物件導向中最精髓的繼承。使用繼承可以大量的減少類似的程式碼,而且對於程式碼的維護上也佔有很大的優勢,今天限量就要來講解如何在 Javascript 實作簡單繼承的機制。


繼承 的概念是基於維持基底類別的原有特性,進而延伸或覆寫,以維持一定的互通性。我們以生態學為範例來解釋:動物,是一個抽象的敘述,但我們一提到動物就會聯想到它的一些特性,像是動物會動, 會吃, 會睡覺...(雖然不一定完全是這樣)。而我們 人 是動物, 狗 是動物, 貓 是動物,所以這些動物物種都有我們想的到的動物行為,但是個別物種不同,相同的行為會有不同的作用,像是狗叫汪汪, 貓叫喵喵, 狗有4條腿, 鳥有2條腿...,這就是物種的多樣性。

程式碼也是一樣的,我們會有類似的結構而個別有不同的行為作用,下面限量就用上一篇的 Pig 範例來說明:

就如前面所說,Animal 是一個抽象的敘述,所以我們先來建一個 Animal 的基底類別結構,將基本的屬性與方法定義好:
// Base Class
var Animal = function() {
    this.name = null;
    this.legCount = null;
    this.init();
};


Animal.prototype.init = function() {
    console.log('Animal init');
};

Animal.prototype.shout = function() {
    console.log('Silence...');
};

Animal.prototype.eat = function() {
    console.log('Food! Food!');
};

限量在 Animal 簡單定義了 name, legCount 的屬性和 init, shout, eat的方法。還記得上一篇說的 Class 嗎?限量在 Animal 的建構子內部設定了 name 與 legCount 的預設值,並呼叫 init 方法,這樣就定義好初始化的基本流程了。再來看看 Pig 如何繼承 Animal:
// Concret Class
var Pig = function(name) {
    Animal.call(this);
    this.name = name || null;
    this.tail = 'circle';
    this.legCount = 4;
};

Pig.prototype = Object.create(Animal.prototype);
Pig.prototype.constructor = Pig;

Pig.prototype.init = function() {
    Animal.prototype.init.call();
    console.log('Pig init');
};

Pig.prototype.shout = function() {
    console.log('Pig! Pig! ' + this.name);
};

Pig.prototype.eat = function() {
    Animal.prototype.eat.call();
    console.log('Pig!');
};

Pig.prototype.sleep = function() {
    console.log('Z~Z~Z...');
};

在 Pig 的實作上,基本繼承 Animal 的屬性與方法都有的,額外又加了 tail 的屬性和 sleep 的方法。另外限量覆寫了 init, shout, eat 方法,在 init 與 eat 的方法中,Animal.prototype.init.call() 表示執行原本 Animal 定義的 init。

其實仔細看程式碼,不難看出實作繼承的原理。原理其實很簡單,就是把 Pig 的 prototype 用 Animal 的 prototype 取代,這樣就可以 Animal 為基底進行擴充。但是這樣還不夠,如果你直接執行會發現程式跑不進去 Pig 的建構子,那是因為 prototype 已經變成 Animal 了,所以我們要把 prototype 的 constructor 指到 Pig 的建構子,然後在 Pig 的建構子內加上 Animal.call() 就能回到 Animal 建構子。
注意:Pig 屬性的初始化要在執行 Animal 建構子結束才可以做,因為在 Animal 建構子裡就有做預設的屬性初始化的動作,如果先在 Pig 建構子做初始化的動作會導致 Animal 預設初始化會蓋掉前面的值。

我們來看看產生一個 Pig 實體會發生什麼事:



看到了沒,結果和想像的一樣,初始化時會先去呼叫 Animal 的 init 然後再執行 Pig 的 init,可以看到除了 Animal 原有的 name 和 legCount,Pig 自身的 tail 也是我們設定的值,另外看看 __proto__ 類型為 Animal 的 prototype,內部的方法也保持 Animal 原有的方法與 Pig 額外的 sleep 方法。

繼承的概念大概就是這樣,有了這個概念相信對從 OO 設計進來 Javascript 的開發人員來說,應該就比較不會那麼怕了。當然 OO 概念還有封裝與多型的概念,這個有機會再來討論吧。

站內相關文章:
Javascript - 物件導向設計(EcmaScript 6之前)






留言