Skip to content

Latest commit

 

History

History
256 lines (184 loc) · 14.5 KB

File metadata and controls

256 lines (184 loc) · 14.5 KB
title slug
JavaScript 中的「繼承」
Learn/JavaScript/Objects/Classes_in_JavaScript

{{LearnSidebar}}{{PreviousMenuNext("Learn/JavaScript/Objects/Object_prototypes", "Learn/JavaScript/Objects/JSON", "Learn/JavaScript/Objects")}}

在解釋過大部分的 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 的所有成員,還要包含:

  1. 新的 subject 屬性,可包含老師所傳授的科目。
  2. 更新過的 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 值,其他參數則指定應該傳送到該函式的參數。

Note

我們此範例中建立新的物件實例時,會指定所要繼承的屬性。但必須注意的是,即使實例不需將屬性指定為參數,你還是必須在建構子中將屬性指定為參數 (在建立物件時,你可能獲得設定為隨意值的屬性)。

所以這裡可在 Teacher() 建構子函式之內有效執行 Person() 建構子函式 (如上述),藉以在 Teacher() 之內定義相同的屬性,但使用的是傳送到 Teacher() 的屬性值,而非 Person() 的屬性值 (我們將 this 值設為簡單的「this」並傳送到 call(),代表這個 thisTeacher() 的函式)。

建構子的最後一行則定義了新的 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() 原型中所定義的函式,該怎麼做呢?

  1. 繼續在現有程式碼下方加入:

    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 主控台以驗證:

    Teacher.prototype.constructor;
  3. 這樣可能會產生問題,所以要設定正確。你可回到自己的原始碼並在最下方加入下列程式碼:

    Teacher.prototype.constructor = Teacher;
  4. 如果儲存並重新整理之後,只要輸入 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() 建構子 (類別) 之上。

Note

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

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

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

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

進階習題

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

Note

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

物件成員摘要

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

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

如果你不確定到底屬於上述的哪一個,也別擔心。現在你還在學習階段,往後定會熟能生巧。

在 JavaScript 中使用繼承的時機?

看到這裡,你心裡應該覺得很複雜。沒錯。「原型」與「繼承」可說是 JavaScript 最複雜的概念之二,但許多 JavaScript 的強大功能與彈性,均來自於其物件結構與繼承,也值得你了解運作方式。

不論是使用 WebAPI 的多樣功能,或是你在字串、陣列等所呼叫的函式\屬性 (定義於內建瀏覽器物件之上),你都可以不斷繼承下去。

在自己的程式碼裡,特別是剛接觸或小型專案時,你用繼承的頻率可能沒那麼高。若沒真正需要,只是「為使用而使用」繼承,老實說只是浪費時間。但隨著程式碼規模越來越大,你就會找到使用的時間。如果你發現自己開始建立類似功能的多個物件時,就可先建立通用的物件類型,來概括所有共用的功能,並在特定物件類型中繼承這些功能,既方便又有效率。

Note

基於 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 則說明得更深入、更精確。

{{PreviousMenuNext("Learn/JavaScript/Objects/Object_prototypes", "Learn/JavaScript/Objects/JSON", "Learn/JavaScript/Objects")}}