MDN wants to learn about developers like you: https://qsurvey.mozilla.com/s3/MDN-dev-survey

翻譯不完整。請協助 翻譯此英文文件

在看完了基本概念之後,接著要說明物件導向 JavaScript (OOJS)。本文將概述物件導向程式設計 (OOP) 的理論,另說明 JavaScript 是如何透過建構子函式來模擬物件類別,又是如何建立物件實體 (Instance)。

必備條件: 基本的電腦素養、已初步了解 HTML 與 CSS、熟悉 JavaScript (參閱〈First steps〉與〈Building blocks〉以及 OOJS 基礎概念 (參閱〈Introduction to objects〉。
主旨: 了解物件導向程式設計的基本理論、其與 JavaScript (幾乎所有東西都是物件) 之間的關係、應如何寫出建構子與物件實體。

物件導向程式設計從高樓起

最先,請你了解物件導向程式設計 (Object-oriented programming;OOP) 最基本的概念。簡單來說,因為 OOP 很容易變得複雜,就算現在就設法完整解釋,也可能讓大家越來越混亂。 OOP 基本概念是:採用objects 來模塑真實的實物世界也就是在程式中的呈現是透過 objects 來塑造其模型,且\或提供簡單方式存取其「難以或不可能採用的功能」。

「Objects/物件」可裝載相關資料與程式碼,資料部分是你對某個 thing 塑造模型的資訊,及程式碼部分用 是操作行為的實現。Object data  -- 函式部分通常也使用 --- 可工整地儲存 (正式一點應該是封裝 Encapsulated) 在物件包裹(這個包裹有特定的稱呼方式,有時候即所謂的命名空間 Namespace) ,使其能輕鬆地建構性存取。物件也常作為「資料儲存 (Datastore),促成簡易實現跨網傳送。

定義物件範本

我們先找個簡單程式,如同學校裡用來顯示師生資訊的程式。本文只是要了解一般的 OOP 理論,並非以特定程式語言為出發點。

我們先拿第一篇物件文章中的「Person」物件類型為範例,其中定義了一個人的一般資料與功能。其實有很多資訊可助你了解一個人 (像是住址、身高、鞋子尺寸、DNA、護照號碼、明顯的人格特質等......),但我們這裡只列出了姓名、年齡、性別、興趣。我們另希望根據這些資料寫出簡介,初步了解這個人。上述這些即所謂的「抽象 (Abstraction)」。為某個複雜東西建立簡單的模型,藉以代表其最重要的概念或特質,且該模型建立方式極易於搭配我們的程式設計用途。

在某些 OOP 語言中,此通用的物件類型定義,即所謂的「類別 (Class)」;但JavaScript 使用了不同的機制與詞彙,下面就會提到。類別並不算是物件,比較像是範本,可定義物件所應具備的參數特性 (Characteristics)。

建立實際物件

我們可從這個「類別」建立物件實體 (Object instance) — 即該物件包含了類別中所定義的資料與功能。而「Person」類別可設定出幾個實際的人物:

在根據類別建立物件實體時,就是執行類別的「建構子 (Constructor) 函式」所建立。而這個「根據類別來建立物件實體」的過程即稱為「實體化 (Instantiation)」。物件實體就是從類別實體化而來。

特殊類別

如果我們不要一般人物,而想建立老師與學生這類比較特定類型的人物。在 OOP 中,我們可根據其他類別建立新的類別。新的子類別則可繼承 (Inherit) 母類別的資料與程式碼特性。你可重複使用所有物件類型共有的功能,而不需再複製之。若功能需與類別有所差異,則可直接於其上定義特殊功能。

這樣很方便。因為老師與學生也同樣使用了如姓名、性別、年齡等的共通特性,所以只要定義這些特性 1 次即可。你也可以分別在不同的類別中定義相同特性,各個特性的定義就置於不同的命名空間中。舉例來說,學生的打招呼方式可以是「Yo, I'm [firstName]」;老師的招呼方式就正式一點,如「Hello, my name is [Prefix] [lastName]」。

注意:所謂的「多型 (Polymorphism)」,即是用多個物件類別建置相同功能。

現在你可根據子類別來建立物件實例了。例如:

本文後面將講解應如何將 OOP 以論實際用於 JavaScript 中。

建構子與物件實例

某些人會說 JavaScript 其實不算是真正的 OO 語言,舉例來說,其他許多 OO 語言是透過「class」這個陳述 (Statement) 來建立類別,但 JS 卻是以所謂的「建構子 (Constructor)」來定義物件及其功能。開發者常常會不知道到底需建立多少物件,這時建構子可讓你高效率建立你所需的物件,並依需要為其附加資料與函式。

在透過建構子建立新的物件實例之後,並不會像「傳統」的 OO 語言一樣,將所有功能複製到新物件之中;而是透過所謂原型鏈 (Prototype chain,可參閱 Object prototypes) 的「參照鏈」,將功能連接至新物件。所以嚴格來說,這不算真正的實例化,而是 JavaScript 透過不同的機制在物件之間分享功能。

注意:不是「傳統 OO 語言」也不見得是壞事。如上面說過的,OOP 可以很快的變得超級複雜。但 JavaScript 則是採用了 OO 的優點卻又不會太過深入。

接著就在 JS 中,透過建構子建立類別及其物件實例。首先請你先在本端磁碟中再另外複製一份前面文章提過的 oojs.html 檔案。

簡易範例

  1. 先看看該如何用一般函式定義 1 個人。將下列函式加到現有程式碼之下:
    function createNewPerson(name) {
      var obj = {};
      obj.name = name;
      obj.greeting = function () {
        alert('Hi! I\'m ' + this.name + '.');
      }
      return obj;
    }
  2. 呼叫此函式之後即可建立新的 1 個人,另在瀏覽器的 JavaScript 主控台中測試下列程式碼:
    var salva = createNewPerson('salva');
    salva.name;
    salva.greeting();
    目前為止沒什麼問題,但有點囉嗦。如果早知道要建立物件的話,又何必要建立新的空白物件再回傳呢?幸好 JavaScript 透過建構子函式提供了方便的捷徑。現在就來試試看!
  3. 用下列程式碼替代之前的函式:
    function Person(name) {
      this.name = name;
      this.greeting = function() {
        alert('Hi! I\'m ' + this.name + '.');
      };
    }

建構子也就是 JavaScript 版本的「類別」之一。你可以注意到,除了無法回傳任何數值或明確建立物件之外,建構子其實已具備函式中的所有功能,並已基本定義了屬性與函式 (Method)。你也能看到這裡同樣用了「this」關鍵字,即不論何時建立了這裡的任一物件實例,物件的「name」屬性均同等於「傳送至建構子呼叫的名稱值」;且 greeting() 函式也會使用相同「傳送至建構子呼叫的名稱值」。

注意:建構子函式名稱往往以大寫字母起頭,如此可方便你在程式碼中找出建構子函式。

我們又該如何呼叫建構子以建立物件呢?

  1. 將下列程式碼加到目前的程式碼之下:
    var person1 = new Person('Bob');
    var person2 = new Person('Sarah');
  2. 儲存程式碼並在瀏覽器中重新載入,試著將下列程式碼輸入到文字輸入畫面中:
    person1.name
    person1.greeting()
    person2.name
    person2.greeting()

現在你應該能在頁面上看到 2 組新物件,且各自以不同的命名空間儲存。若要存取其屬性與函式,就要以 person1person2 開始呼叫。這些物件均完整封包,不致與其他功能衝突;但仍具備相同的 name 屬性與 greeting() 函式。另請注意,物件均使用當初建立時所各自指派的 name 值;這也是「this」如此重要的原因之一,以確保物件可使用自己的值而不致混淆其他數值。

再看一次建構子呼叫:

var person1 = new Person('Bob');
var person2 = new Person('Sarah');

這裡用了「new」關鍵字告知瀏覽器「我們要建立新的物件實例」, followed by the function name with its required parameters contained in parentheses, ,並將結果儲存於變數之中 — 如此極類似標準功能的呼叫方式。各個實例均根據此定義所建立:

function Person(name) {
  this.name = name;
  this.greeting = function() {
    alert('Hi! I\'m ' + this.name + '.');
  };
}

在建立新的物件之後,person1person2 的變數將有效率地納入下列物件:

{
  name : 'Bob',
  greeting : function() {
    alert('Hi! I\'m ' + this.name + '.');
  }
}

{
  name : 'Sarah',
  greeting : function() {
    alert('Hi! I\'m ' + this.name + '.');
  }
}

剛剛說的「有效率」,是因為實際功能仍定義於類別中,而非物件實例之中。這情況與我們稍早談過的物件實字 (Object literal) 相反。

建立完整的建構子

上面不過是入門的簡單範例。接著繼續建立最後的 Person() 建構子。

  1. 將截至目前為止的程式碼移除,加入下列取代用的建構子。原則上與簡易範例完全一樣,只是比較複雜一點:
    function Person(first, last, age, gender, interests) {
      this.name = {
        first,
        last
      };
      this.age = age;
      this.gender = gender;
      this.interests = interests;
      this.bio = function() {
        alert(this.name.first + ' ' + this.name.last + ' is ' + this.age + ' years old. He likes ' + this.interests[0] + ' and ' + this.interests[1] + '.');
      };
      this.greeting = function() {
        alert('Hi! I\'m ' + this.name.first + '.');
      };
    };
  2. 再接著加入下列程式碼,就可建立物件實例:
    var person1 = new Person('Bob', 'Smith', 32, 'male', ['music', 'skiing']);

現在可存取我們為第一個物件所定義的屬性與函式:

person1['age']
person1.interests[1]
person1.bio()
// etc.

注意:如果你到這裡有點吃力,請先比較自己與我們的程式碼。可參閱 oojs-class-finished.html (也可看實際執行的情況)。

進階習題

在開始之前,先試著自己添加更多物件建立程式碼,再針對產生的物件實例取得並設定其成員。

此外,原來的 bio() 函式其實有點問題。即使你的「人」是女性,其輸出一定會有「He」這個代名詞。而且即使 interests 陣列中列出超過 2 個以上的「興趣」,這個 bio 函式也只會有 2 個興趣。你能試著在類別定義 (建構子) 中修正這個問題嗎?你可在建構子中放入任何你喜歡的程式碼 (但可能會需要幾個 conditionals 搭配 1 個迴圈)。想想應如何根據性別以及列出的興趣 (1 或 2 個以上),建構出不同的程式碼。

注意:如果你卡在這裡,我們也在 GitHub repo 上提供了解答 (立刻觀看)。先試著自己解決問題吧!

建立物件實例的其他方法

目前解釋了 2 種建立物件實例的方法 — 宣告物件實字,以及使用建構子函式。

當然還有別的方法,但我們希望你先熟悉此 2 種方法,以免你日後的 Web 旅程上會再遇到。

Object() 建構子

首先可使用 Object() 建構子建立新的物件。沒錯,即使是泛型物件 (Generic object) 也具備建構子,可用以產生空白物件。

  1. 將下列輸入瀏覽器的 JavaScript 主控台內:
    var person1 = new Object();
  2. 如此會在 person1 變數中儲存 1 組空白物件。接著可透過點 (dot-) 或括弧記法 (bracket notation) 為此物件新增屬性與函式。如下列範例:
    person1.name = 'Chris';
    person1['age'] = 38;
    person1.greeting = function() {
      alert('Hi! I\'m ' + this.name + '.');
    }
  3. 你可將 1 組物件實字傳送給 Object() 建構子作為參數,藉以預先填入屬性\函式。如下所示:
    var person1 = new Object({
      name : 'Chris',
      age : 38,
      greeting : function() {
        alert('Hi! I\'m ' + this.name + '.');
      }
    });

使用 create() 函式

JavaScript 內建的 1 組函式為 create(),可讓你根據現有物件建立新的物件實例:

  1. 在 JavaScript 主控台裡測試:
    var person2 = Object.create(person1);
  2. 再測試以下:
    person2.name
    person2.greeting()

你會看到 person2 是根據 person1 所建立,並具備相同的屬性與函式,因此你不需再定義建構子,即可建立新的物件實例。缺點就是瀏覽器並不支援 create(),不如建構子的普及程度 (IE9, as opposed to IE8 or even before),而且建構子比較不會讓你的程式碼顯得紊亂。開發者可在特定一處建立建構子,再依需要建立實例,這樣就很清楚實例的出處。

如果你不太在意舊版瀏覽器的支援問題,也只需要將物件複製幾次,那麼建立建構子可能反而會破壞你的程式碼。就看你自己喜好而定,但某些人就發現 create() 比較易於了解並使用。

我們後續會再說明 create() 的效果。

總結

本文簡略說明了 OO 理論,雖然還沒全部講完,至少也讓你初步了解到本文所要闡述的重點。此外,我們已經開始說明 JavaScript 與 OO 之間的關係、其與「傳統 OO」之間的差異、使用建構子於 JavaScript 中實作類別的方法,以及其他產生物件實例的方式。

下一篇文章將說明 JavaScript 物件原型。

文件標籤與貢獻者

 此頁面的貢獻者: cjchng, royvbtw, MashKao, iigmir
 最近更新: cjchng,