在上一篇限量介紹了在 ES6 之前如何在 Javascript 建立類別與 Namespace,但是還沒講到物件導向中最精髓的繼承。使用繼承可以大量的減少類似的程式碼,而且對於程式碼的維護上也佔有很大的優勢,今天限量就要來講解如何在 Javascript 實作簡單繼承的機制。
繼承 的概念是基於維持基底類別的原有特性,進而延伸或覆寫,以維持一定的互通性。我們以生態學為範例來解釋:動物,是一個抽象的敘述,但我們一提到動物就會聯想到它的一些特性,像是動物會動, 會吃, 會睡覺...(雖然不一定完全是這樣)。而我們 人 是動物, 狗 是動物, 貓 是動物,所以這些動物物種都有我們想的到的動物行為,但是個別物種不同,相同的行為會有不同的作用,像是狗叫汪汪, 貓叫喵喵, 狗有4條腿, 鳥有2條腿...,這就是物種的多樣性。
程式碼也是一樣的,我們會有類似的結構而個別有不同的行為作用,下面限量就用上一篇的 Pig 範例來說明:
就如前面所說,Animal 是一個抽象的敘述,所以我們先來建一個 Animal 的基底類別結構,將基本的屬性與方法定義好:
限量在 Animal 簡單定義了 name, legCount 的屬性和 init, shout, eat的方法。還記得上一篇說的 Class 嗎?限量在 Animal 的建構子內部設定了 name 與 legCount 的預設值,並呼叫 init 方法,這樣就定義好初始化的基本流程了。再來看看 Pig 如何繼承 Animal:
在 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之前)
繼承 的概念是基於維持基底類別的原有特性,進而延伸或覆寫,以維持一定的互通性。我們以生態學為範例來解釋:動物,是一個抽象的敘述,但我們一提到動物就會聯想到它的一些特性,像是動物會動, 會吃, 會睡覺...(雖然不一定完全是這樣)。而我們 人 是動物, 狗 是動物, 貓 是動物,所以這些動物物種都有我們想的到的動物行為,但是個別物種不同,相同的行為會有不同的作用,像是狗叫汪汪, 貓叫喵喵, 狗有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之前)
留言
張貼留言