JavaScript 中的「繼承」

在解釋過大部分的 OOJS 細節之後,本文將說明該如何建立「子」物件類別 (建構子),並從其「母」類別繼承功能。此外,也將建議開發者應於何時、於何處使用 OOJS。

必備條件: 基本的電腦素養、已了解 HTML 與 CSS 基本概念、熟悉 JavaScript 基礎 (可參閱〈First steps〉與〈Building blocks〉) 與 OOJS 的基礎 (可參閱〈Introduction to objects (en-US)〉)。
主旨: 了解應如何建構 JavaScript 中的繼承。

原型繼承

目前為止,我們看過幾個繼承的實作範例:原型鍊的運作方式,以及繼承的成員如何形成原型鍊。但這些大部分都與內建的瀏覽器函式有關。那我們又該如何在 JavaScript 建立物件且由其他物件繼承而來呢?

如稍早的系列文章中所述,某些人認為 JavaScript 並非真正的物件導向 (OO) 語言。在「典型 OO」中,你必須定義特定的類別物件,才能定義哪些類別所要繼承的類別 (可參閱〈C++ inheritance〉了解簡易範例)。JavaScript 則使用不同的系統 —「繼承」的物件並不會一併複製功能過來,而是透過原型鍊連接其所繼承的功能,亦即所謂的原型繼承 (Prototypal inheritance)。

就透過範例了解此一概念吧。

入門

首先將 oojs-class-inheritance-start.html 檔案 (亦可參考實際執行) 複製到本端磁碟。可在裡面找到本課程一直沿用的 Person() 建構子範例,但這裡有些許不同,也就是該建構子只定義了屬性:

js
function Person(first, last, age, gender, interests) {
  this.name = {
    first,
    last,
  };
  this.age = age;
  this.gender = gender;
  this.interests = interests;
}

這些函式均已定義於建構子的原型之上,例如:

js
Person.prototype.greeting = function () {
  alert("Hi! I'm " + this.name.first + ".");
};

假設要建立一個像前面 OO 定義中說過的 Teacher 類別,且除了繼承 Person 的所有成員,還要包含:

  1. 新的 subject 屬性,可包含老師所傳授的科目。
  2. 更新過的 greeting() 函式,要比標準的 greeting() 函式更正式一點,更適合老師在校對學生使用。

定義 Teacher() 建構子函式

首先必須建立 Teacher() 建構子,將下列程式碼加到現有程式碼之下:

js
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(),代表這個 thisTeacher() 的函式)。

建構子的最後一行則定義了新的 subject 屬性,代表只有老師具備,一般人不會有。

我們也可以單純這樣做:

js
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() 原型中所定義的函式,該怎麼做呢?

  1. 繼續在現有程式碼下方加入:
    js
    Teacher.prototype = Object.create(Person.prototype);
    
    這裡再次用好朋友 create() 解救。我們透過 create() 並搭配等同於 Person.prototype 的原型,建立新的 prototype 屬性值 (它本身就是物件,包含屬性與函式) ,並將之設定為 Teacher.prototype 的值。也就是說 Teacher.prototype 現在會繼承 Person.prototype 上的所有可用函式。
  2. 此外,基於我們的繼承方式,Teacher() prototype 的建構子屬性目前設定為 Person()。可參閱 Stack Overflow post 進一步了解原因。可先儲存自己的程式碼、在瀏覽器中載入頁面,再將下列程式碼輸入至 JavaScript 主控台以驗證:
    js
    Teacher.prototype.constructor;
    
  3. 這樣可能會產生問題,所以要設定正確。你可回到自己的原始碼並在最下方加入下列程式碼:
    js
    Teacher.prototype.constructor = Teacher;
    
  4. 如果儲存並重新整理之後,只要輸入 Teacher.prototype.constructor 應該就會回傳 Teacher()

給 Teacher() 新的 greeting() 函式

接著必須在 Teacher() 建構子上定義新的 greeting() 函式。

最簡單的方法就是在 Teacher() 的原型上定義此函式。將下列加入程式碼最底部:

js
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() 建立物件實例。將下列 (或是你想用的類似程式碼) 加入現有程式碼的底部:

js
var teacher1 = new Teacher(
  "Dave",
  "Griffiths",
  31,
  "male",
  ["football", "cookery"],
  "mathematics",
);

儲存並重新整理之後,試著存取新 teacher1 物件的屬性語函式,例如:

js
teacher1.name.first;
teacher1.interests[0];
teacher1.bio();
teacher1.subject;
teacher1.greeting();

這樣應該運作無虞。前 3 行的存取成員即繼承自一般 Person() 建構子 (類別)。最後 2 行的存取成員只能用於特定的 Teacher() 建構子 (類別) 之上。

備註: 如果你無法進行到現有進度,可比較自己與完整版本 (亦可看實際執行的情形) 的程式碼。

這裡所提的技巧,當然不是在 JavaScript 建立繼承類別的唯一方法,但足以堪用。並可讓你了解應如何於 JavaScript 實作繼承。

你可能也想看看某些新的 ECMAScript 功能,可更簡潔的在 JavaScript 中繼承 (參閱 Classes)。但由於這些功能尚未廣泛支援其他瀏覽器,這裡先略過不提。本系列文章中提到的其他程式碼,均可回溯支援到 IE9 或更早版本。當然還是有辦法支援更舊的版本。

一般方法就是使用 JavaScript 函式庫,且最常見的就是簡單集結可用的功能,更快、更輕鬆的完成繼承。例如 CoffeeScript 即提供了 classextends 等等。

進階習題

〈OOP 理論〉段落中,我們也納入了 Student 類別並繼承了 Person 的所有功能,此外也提供不同的 greeting() 函式,且較 Teacher 的問候語沒那麼正式。在看了該段落中的學生問候語之後,可試著實作自己的 Student() 建構子,並繼承 Person(), 的所有功能,再實作不同的 greeting() 函式。

備註: 如果你無法進行到現有進度,可參考完成版本 (亦可看實際執行的情形)。

物件成員摘要

簡單來說,你基本上需要考量 3 種類型的屬性\函式:

  1. 已於建構子函式中定義,會交給物件實體的屬性\函式。這應該很容易處理。在你自己的程式碼中,就是透過 this.x = x 類別行並在建構子中定義的成員;在瀏覽器程式碼中,就是僅限物件實體可用的成員 (一般是透過 new 關鍵字並呼叫建構子所建立,例如 var myInstance = new myConstructor())。
  2. 直接在建構子上定義,並僅能用於該建構子的屬性\函式。這類屬性\函式往往只能用於內建瀏覽器物件之上,並直接「鍊接」至建構子 (而非實例) 以利識別,例如 Object.keys()
  3. 定義於建構子原型上的屬性\函式,交由所有的實例繼承,亦繼承了物件類別。這類屬性\函式包含建構子原型屬性之上定義的所有成員,例如 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 則說明得更深入、更精確。