JavaScript 中的「繼承」
在解釋過大部分的 OOJS 細節之後,本文將說明該如何建立「子」物件類別 (建構子),並從其「母」類別繼承功能。此外,也將建議開發者應於何時、於何處使用 OOJS。
必備條件: | 基本的電腦素養、已了解 HTML 與 CSS 基本概念、熟悉 JavaScript 基礎 (可參閱〈First steps〉與〈Building blocks〉) 與 OOJS 的基礎 (可參閱〈Introduction to objects〉)。 |
---|---|
主旨: | 了解應如何建構 JavaScript 中的繼承。 |
原型繼承
目前為止,我們看過幾個繼承的實作範例:原型鍊的運作方式,以及繼承的成員如何形成原型鍊。但這些大部分都與內建的瀏覽器函式有關。那我們又該如何在 JavaScript 建立物件且由其他物件繼承而來呢?
如稍早的系列文章中所述,某些人認為 JavaScript 並非真正的物件導向 (OO) 語言。在「典型 OO」中,你必須定義特定的類別物件,才能定義哪些類別所要繼承的類別 (可參閱〈C++ inheritance〉了解簡易範例)。JavaScript 則使用不同的系統 —「繼承」的物件並不會一併複製功能過來,而是透過原型鍊連接其所繼承的功能,亦即所謂的原型繼承 (Prototypal inheritance)。
就透過範例了解此一概念吧。
入門
首先將 oojs-class-inheritance-start.html 檔案 (亦可參考實際執行) 複製到本端磁碟。可在裡面找到本課程一直沿用的 Person()
建構子範例,但這裡有些許不同,也就是該建構子只定義了屬性:
function Person(first, last, age, gender, interests) {
this.name = {
first,
last,
};
this.age = age;
this.gender = gender;
this.interests = interests;
}
這些函式均已定義於建構子的原型之上,例如:
Person.prototype.greeting = function () {
alert("Hi! I'm " + this.name.first + ".");
};
假設要建立一個像前面 OO 定義中說過的 Teacher
類別,且除了繼承 Person
的所有成員,還要包含:
- 新的
subject
屬性,可包含老師所傳授的科目。 - 更新過的
greeting()
函式,要比標準的greeting()
函式更正式一點,更適合老師在校對學生使用。
定義 Teacher() 建構子函式
首先必須建立 Teacher()
建構子,將下列程式碼加到現有程式碼之下:
function Teacher(first, last, age, gender, interests, subject) {
Person.call(this, first, last, age, gender, interests);
this.subject = subject;
}
這看起來和 Person 建構子有許多地方類似,但比較奇怪的就是之前沒看過的 call()
函式。此函式基本上可讓你呼叫在其他地方定義的函數,而非目前內文 (context) 中定義的函式。當執行函式時,第一個參數用來指定你要使用 this
值,其他參數則指定應該傳送到該函式的參數。
備註:我們此範例中建立新的物件實例時,會指定所要繼承的屬性。但必須注意的是,即使實例不需將屬性指定為參數,你還是必須在建構子中將屬性指定為參數 (在建立物件時,你可能獲得設定為隨意值的屬性)。
所以這裡可在 Teacher()
建構子函式之內有效執行 Person()
建構子函式 (如上述),藉以在 Teacher()
之內定義相同的屬性,但使用的是傳送到 Teacher()
的屬性值,而非 Person()
的屬性值 (我們將 this
值設為簡單的「this」並
傳送到 call()
,代表這個 this
是 Teacher()
的函式)。
建構子的最後一行則定義了新的 subject
屬性,代表只有老師具備,一般人不會有。
我們也可以單純這樣做:
function Teacher(first, last, age, gender, interests, subject) {
this.name = {
first,
last,
};
this.age = age;
this.gender = gender;
this.interests = interests;
this.subject = subject;
}
但這樣只是重新定義了新的屬性,而不是繼承自 Person()
而來,所以無法達到我們預設的目標,也需要更多程式碼才能達成。
設定 Teacher() 的原型與建構子參考
到目前為止發現一個問題:我們定義了新的建構子,但預設只有 1 個空白的 prototype
屬性。接著要讓 Teacher()
繼承 Person()
原型中所定義的函式,該怎麼做呢?
-
繼續在現有程式碼下方加入:
jsTeacher.prototype = Object.create(Person.prototype);
這裡再次用好朋友
create()
解救。我們透過create()
並搭配等同於Person.prototype
的原型,建立新的prototype
屬性值 (它本身就是物件,包含屬性與函式) ,並將之設定為Teacher.prototype
的值。也就是說
Teacher.prototype
現在會繼承Person.prototype
上的所有可用函式。 -
此外,基於我們的繼承方式,
Teacher()
prototype
的建構子屬性目前設定為Person()。可參閱
Stack Overflow post 進一步了解原因。可先儲存自己的程式碼、在瀏覽器中載入頁面,再將下列程式碼輸入至 JavaScript 主控台以驗證:jsTeacher.prototype.constructor;
-
這樣可能會產生問題,所以要設定正確。你可回到自己的原始碼並在最下方加入下列程式碼:
jsTeacher.prototype.constructor = Teacher;
-
如果儲存並重新整理之後,只要輸入
Teacher.prototype.constructor
應該就會回傳Teacher()
。
給 Teacher() 新的 greeting() 函式
接著必須在 Teacher()
建構子上定義新的 greeting()
函式。
最簡單的方法就是在 Teacher()
的原型上定義此函式。將下列加入程式碼最底部:
Teacher.prototype.greeting = function () {
var prefix;
if (
this.gender === "male" ||
this.gender === "Male" ||
this.gender === "m" ||
this.gender === "M"
) {
prefix = "Mr.";
} else if (
this.gender === "female" ||
this.gender === "Female" ||
this.gender === "f" ||
this.gender === "F"
) {
prefix = "Mrs.";
} else {
prefix = "Mx.";
}
alert(
"Hello. My name is " +
prefix +
" " +
this.name.last +
", and I teach " +
this.subject +
".",
);
};
這樣就會顯示老師的問候語,也會針對老師的性別使用合適的稱呼,可用於條件陳述式。
簡易範例
現在你已經輸入所有程式碼了,可以試著用Teacher()
建立物件實例。將下列 (或是你想用的類似程式碼) 加入現有程式碼的底部:
var teacher1 = new Teacher(
"Dave",
"Griffiths",
31,
"male",
["football", "cookery"],
"mathematics",
);
儲存並重新整理之後,試著存取新 teacher1
物件的屬性語函式,例如:
teacher1.name.first;
teacher1.interests[0];
teacher1.bio();
teacher1.subject;
teacher1.greeting();
這樣應該運作無虞。前 3 行的存取成員即繼承自一般 Person()
建構子 (類別)。最後 2 行的存取成員只能用於特定的 Teacher()
建構子 (類別) 之上。
這裡所提的技巧,當然不是在 JavaScript 建立繼承類別的唯一方法,但足以堪用。並可讓你了解應如何於 JavaScript 實作繼承。
你可能也想看看某些新的 ECMAScript 功能,可更簡潔的在 JavaScript 中繼承 (參閱 Classes)。但由於這些功能尚未廣泛支援其他瀏覽器,這裡先略過不提。本系列文章中提到的其他程式碼,均可回溯支援到 IE9 或更早版本。當然還是有辦法支援更舊的版本。
一般方法就是使用 JavaScript 函式庫,且最常見的就是簡單集結可用的功能,更快、更輕鬆的完成繼承。例如 CoffeeScript 即提供了 class
、extends
等等。
進階習題
在〈OOP 理論〉段落中,我們也納入了 Student
類別並繼承了 Person
的所有功能,此外也提供不同的 greeting()
函式,且較 Teacher
的問候語沒那麼正式。在看了該段落中的學生問候語之後,可試著實作自己的 Student()
建構子,並繼承 Person()
, 的所有功能,再實作不同的 greeting()
函式。
物件成員摘要
簡單來說,你基本上需要考量 3 種類型的屬性\函式:
- 已於建構子函式中定義,會交給物件實體的屬性\函式。這應該很容易處理。在你自己的程式碼中,就是透過
this.x = x
類別行並在建構子中定義的成員;在瀏覽器程式碼中,就是僅限物件實體可用的成員 (一般是透過new
關鍵字並呼叫建構子所建立,例如var myInstance = new myConstructor()
)。 - 直接在建構子上定義,並僅能用於該建構子的屬性\函式。這類屬性\函式往往只能用於內建瀏覽器物件之上,並直接「鍊接」至建構子 (而非實例) 以利識別,例如
Object.keys()
。 - 定義於建構子原型上的屬性\函式,交由所有的實例繼承,亦繼承了物件類別。這類屬性\函式包含建構子原型屬性之上定義的所有成員,例如
myConstructor.prototype.x()
。
如果你不確定到底屬於上述的哪一個,也別擔心。現在你還在學習階段,往後定會熟能生巧。
在 JavaScript 中使用繼承的時機?
看到這裡,你心裡應該覺得很複雜。沒錯。「原型」與「繼承」可說是 JavaScript 最複雜的概念之二,但許多 JavaScript 的強大功能與彈性,均來自於其物件結構與繼承,也值得你了解運作方式。
不論是使用 WebAPI 的多樣功能,或是你在字串、陣列等所呼叫的函式\屬性 (定義於內建瀏覽器物件之上),你都可以不斷繼承下去。
在自己的程式碼裡,特別是剛接觸或小型專案時,你用繼承的頻率可能沒那麼高。若沒真正需要,只是「為使用而使用」繼承,老實說只是浪費時間。但隨著程式碼規模越來越大,你就會找到使用的時間。如果你發現自己開始建立類似功能的多個物件時,就可先建立通用的物件類型,來概括所有共用的功能,並在特定物件類型中繼承這些功能,既方便又有效率。
備註:基於 JavaScript 運作的方式 (如原型鍊等),物件之間的功能共享一般稱為「委託 (Delegation)」,即特定物件將功能委託至通用物件類型。「委託」其實比繼承更精確一點。因為「所繼承的功能」並不會複製到「進行繼承的物件」之上,卻是保留在通用物件之中。
當使用繼承時,建議你不要設定太多層的繼承關係,並時時留意你所定義的函式與屬性。在開始寫程式碼時,你可能會暫時修改內建瀏覽器物件的原型,除非你真的需要,否則儘量別這麼做。太多繼承可能連你自己都搞混,而且一旦需要除錯就會痛苦萬分。
最後,物件可說是另一種形式的程式碼再利用,如同函式或迴圈一般,且有其專屬的角色與優點。如果你正建立一堆相關變數與函式,並要全部一起追蹤、封裝,就可以透過物件。你也能以物件方式傳送整個資料集合。而且上述兩種情況均不需使用建構子或繼承就能夠達成。如果你只需要某一物件的單一實作,那麼單純使用物件就好,完全不需要繼承。
摘要
本文為大家溫習了 OOJS 核心理論和語法。現在你應該了解 JavaScript 物件與 OOP 的基本概念、原型及原型繼承、建立類別 (建構子) 與物件實例的方法、為類別新增功能、建立從其他類別繼承而來的子類別。
下篇文章就要來看看該如何搭配 JavaScript Object Notation (JSON),使用 JavaScript 物件的常見資料交換格式。
另可參閱
- ObjectPlayground.com — 互動式學習網站,讓你了解物件。
- Secrets of the JavaScript Ninja 的第六章 — 進階 JavaScript 概念與技術的好書。第六章講述了原型與繼承的概念。此書有紙本與線上版。
- You Don't Know JS: this & Object Prototypes — Kyle Simpson 絕佳 JavaScript 手冊系列的一部分。第五章特別深入講述了原型。我們透過本文為初學者提供簡易概念,但 Kyle 則說明得更深入、更精確。