JavaScript 物件導向介紹

深入淺出物件導向,JavaScript 支援強大、彈性的物件導向程式設計 (OOP)。本篇文章會先介紹物件導向程式設計,然後複習 JavaScript 物件模型,最後示範在 JavaScript 物件導向程式設計的概念。本篇文章並不會介紹 在 ECMAScript 6 的物件導向程式設計 的新語法。

複習 JavaScript

若您對 JavaScript 變數、型態、函數及作用範圍的觀念並不是很有信心,您可以閱讀 重新膫解 JavaScript 中相關的主題。您也可以參考 JavaScript 指南

物件導向程式設計

物件導向程式設計 (Object-oriented programming, OOP) 是一種使用 abstraction 概念表達現實世界的程式設計方式。物件導向程式設計運用數個先前所建立的技術所組成,包含模組化 (modularity)、多型 (polymorphism) 以及封裝 (encapsulation) 。直到今天許多主流的程式語言 (如 Java, JavaScript, C#, C++, Python, PHP, Ruby 與 Objective-C) 也都支援物件導向程式設計。

物件導向程式設計是將軟體想像成由一群物件交互合作所組成,而非以往以函數 (Function) 或簡單的指令集交互合作所組成。在物件導向的架構中,每個物件都具有接收訊息,處理資料以及發送訊息給其他物件的能力。每個物件都可視為獨一無二的個體,他們扮演不同的角色並有不同的能力及責任。

物作導向程式設計提出了一個一個更有彈性且易於維護的設計方法,且廣泛被許多大型軟體工程所採用。由於物件導向程式設計強調模組化,因為物件導向的程式碼變的較為容易開發且易於理解。與較少模組化的程式設計技術相比,物件導向的程式碼更能更直接分析、編寫、理解複雜的情況與程序。1

JavaScript 是一個以雛型為基礎 (Prototype-based) 的程式設計語言 (或更準確的說是以雛型為基礎的腳本語言),它採用了複製的模式而非繼承。以雛型為基礎的程式設計語言是一種物件導向程式設計,使用了函數來當做類別 (Class) 的建構子 (Constructor),儘管 JavaScript 擁有類別 (Class) 的關鍵字,但它沒有類別敘述,當要拿 JavaScript 與其他物件導向程式語言相比時,這是很重要的區別。

專門用語

Namespace
可讓開發人員包裝所有功能到一個獨一無二、特定應用程式名稱的容器。
Class
用來定義物件的特徵,類別 (Class) 是物件屬性與方法的藍圖。
Object
類別 (Class) 的實際案例。
Property
物件 (Object) 的特徵,例如:顏色。
Method
物件 (Object) 的功能,例如:行走。它是與類別相關的子程序或函數。
Constructor
一個在物件產生時會被呼叫的方法。通常會使用與其所在類別 (Class) 相同的名稱。
Inheritance
一個類別 (Class) 可以繼承另一個類別的特徵與功能。
Encapsulation
可以將資料與方法包裝在一起使用的技術。
Abstraction
結合物件的複雜繼承關係、方法與屬性來充分反映現實的模型。
Polymorphism
Poly 指的是 "多" 而 Morphism 指的是 ""。是指不同的類別可以定義相同的方法或屬性。

要了解物件導向程式設計更廣泛的說明,請參考維基百科的  Object-oriented programming

以雛型為基礎 (Prototype-based) 的程式設計

以雛型為基礎的程式設計是一種不使用類別的物件導向程式設計模式,但它是第一個透過修改 (或者擴充) 既有的 prototype 來達到類別的功能並可重複使用 (等同在以類別為基礎的程式語言中的繼承)。 又稱作無類別 (Classless)、雛型導向 (Prototype-oriented) 或以實例為基的程式語言 (Instance-based programming)。

最早 (最典型) 以雛型為基礎的程式語言的典範是由 David Ungar 與 Randall Smith 所開發的 Self。近年來無類別 (Class-less) 的程式設計風格越來越流行,並且被 JavaScript, Cecil, NewtonScript, Io, MOO, REBOL, Kevo, Squeak (在使用 Viewer 框架來處理 Morphic 元件時),還有許多其他程式語言所採用。2

JavaScript 物件導向程式設計

命名空間

命名空間是一個可讓開發人員包裝所有功能到一個獨一無二、特定應用程式名稱的容器。在 JavaScript 中命名空間其實是另一個包含方法、屬性及物件的物件。

注意,在 JavaScript 中一般物件與命名空間並無語法上的差異,這於其他許多物件導向的語言並不相同,可能是初學 JavaScript 的程式設計師容易混淆的地方。

在 JavaScript 建立一個命名空間背後的概念非常的簡單:建立一個全域的物件,然後將所有的變數、方法及函數設為該物件的屬性。使用命名空間可以減少應用程式中名稱衝突發生的機率,由於每個應用程式的物件皆會是應用程式定義的全域物件的屬性。

讓我們來建立一個叫做 MYAPP 全域物件:

// 全域命名空間
var MYAPP = MYAPP || {};

在上述程式範例,我們會先檢查 MYAPP 是否已經定義過 (不論是定義在同一檔案或在其他檔案)。若已定義過,便會使用現有的 MYAPP 全域物件,否則會建一個稱作 MYAPP 的空物件來包裝方法、函數、變數及物件。

我們也可以建立子命名空間 (要注意,全域物件必須已事先定義):

// 子命名空間
MYAPP.event = {};

以下的程式碼會建立一個命名空間並加入變數、函數以及一個方法:

// 建立一個稱作 MYAPP.commonMethod 的容器來存放常用方法與屬性
MYAPP.commonMethod = {
  regExForName: "", // define regex for name validation
  regExForPhone: "", // define regex for phone no validation
  validateName: function(name){
    // Do something with name, you can access regExForName variable
    // using "this.regExForName"
  },
 
  validatePhoneNo: function(phoneNo){
    // do something with phone number
  }
}

// 物件與方法宣告
MYAPP.event = {
    addListener: function(el, type, fn) {
    // code stuff
    },
    removeListener: function(el, type, fn) {
    // code stuff
    },
    getEvent: function(e) {
    // code stuff
    }
  
    // 可以加入其他方法與屬性
}

// 使用 addListener 方法的語法:
MYAPP.event.addListener("yourel", "type", callback);

標準內建物件

 JavaScript 的核心內建了許多物件,例如有 Math, Object, Array 以及 String。以下範例將示範如何使用 Math 物件中的 random() 方法來取得一個隨機的數字。

console.log(Math.random());
注意:這個例子及之後的例子會假設全域已經有定義名稱為 console.log() 的函數。console.log() 函數並不算是 JavaScript 的一部份,但是有許多瀏覽器會實作這個功能來協助除錯使用。

請參考 JavaScript 參考: 標準內建物件 來取得在 JavaScript 中所有核心物件的清單。

每個在 JavaScript 中的物件均為物件 Object 實例 (Instance),因此會繼承其所有的屬性與方法。

自訂物件

類別 (Class)

JavaScript 是以雛形為基礎的程式語言,並沒有像在 C++ 或 Java 中看以到的 class 敘述句,這有時會讓習慣使用有 class 敘述句的程式設計師混淆。JavaScript 使用函數來作為類別 (Class) 的建構子 (Constructor),要定義一個類別 (Class) 如同定義一個函數 (Function)一樣簡單,以下例子中我們使用空的建構子來定義了一個新的 Person 類別。

var Person = function () {};

物件 (Object) - 類別的實例 (Instance)

要建立物件 obj 的新實例,我們可以使用 new obj 敘述句,並將結果 (型態為 obj) 指派 (Assign) 到一個變數方便之後存取。

在先前翻例中我們定義了一個名稱為 Person 的類別 (Class)。在以下的例子我們會建立兩個實例 (person1person2)。

var person1 = new Person();
var person2 = new Person();

請參考 Object.create() 來了解建立未初始化實例的實例化新方法。

建構子 (Constructor)

建構子會在實例化 (Instantiation) 時被呼叫 (建立物件實例被建立時)。建構子是類別的一個方法,在 JavaScript 中會以函數來當做物件的建構子,因此無須明確的定義建構子方法,而每個在類別中宣告的動作均會在實例化時被執行。

建構子會用來設定物件的屬性 (Property) 或是呼叫要準備讓物件可以使用的方法 (Method)。增加類別的方法及定義會使用另一種語法,在本文稍後會說明。

在以下例之中,類別 Person 的建構子在 Person 實例化時會記錄下一個訊息。

var Person = function () {
  console.log('instance created');
};

var person1 = new Person(); // 會記錄 "instance created"
var person2 = new Person(); // 會記錄 "instance created"

屬性 (Property) - 物件的屬性

屬性即為在類別中的變數,每個物件的實例都會有同樣的屬性。屬性會在類別的建構子 (函數) 中設定,所以屬性在每個實例產生時才會產生。

關鍵字 this 可以引用目前的物件,讓您使用在該類別中的其他屬性。存取 (讀寫或寫入) 一個在類別之外的屬性可以用語法:InstanceName.Property,如同在 C++, Java 以及其他語言。 (在類別內會使用語法 this.Property 來取得或設定屬性的數值。)

在以下例子中,我們會在實例化時定義 Person 類別的 firstName 屬性:

var Person = function (firstName) {
  this.firstName = firstName;
  console.log('Person instantiated');
};

var person1 = new Person('Alice'); // 會記錄 "Person instantiated"
var person2 = new Person('Bob'); // 會記錄 "Person instantiated"

// 顯示物件的 firstName 屬性
console.log('person1 is ' + person1.firstName); // 會記錄 "person1 is Alice"
console.log('person2 is ' + person2.firstName); // 會記錄 "person2 is Bob"

方法 (Method)

方法即為函數 (也如同函數般定義),但是依照屬性的邏輯來運作,呼叫一個方法如同存取一個屬性,但您需要在函數名稱後加上 () ,並有可能會有參數。要定義一個方法,只需將函數指定 (Assign) 給類別的 prototype 屬性中一個已命名的屬性,接著,您便可用剛指定的屬性名稱來呼叫該物件的方法。

以下範例中,我們為 Person  類別定義了方法 sayHello() 並使用。

var Person = function (firstName) {
  this.firstName = firstName;
};

Person.prototype.sayHello = function() {
  console.log("Hello, I'm " + this.firstName);
};

var person1 = new Person("Alice");
var person2 = new Person("Bob");

// 呼叫 Person sayHello 方法。
person1.sayHello(); // 會記錄 "Hello, I'm Alice"
person2.sayHello(); // 會記錄 "Hello, I'm Bob"

在 JavaScript 中,方法其實是一般的函數物件 (Function object) 連結到一個物件的屬性,這意謂著您可以在  "物件之外" 呼叫方法。請看以下範例程式碼:

var Person = function (firstName) {
  this.firstName = firstName;
};

Person.prototype.sayHello = function() {
  console.log("Hello, I'm " + this.firstName);
};

var person1 = new Person("Alice");
var person2 = new Person("Bob");
var helloFunction = person1.sayHello;

// 會記錄 "Hello, I'm Alice"
person1.sayHello();

// 會記錄 "Hello, I'm Bob"
person2.sayHello();

// 會記錄 "Hello, I'm undefined" (或在 Strict 
// 模式會出現 TypeError)
helloFunction();                                    

// 會記錄 true
console.log(helloFunction === person1.sayHello);

// 會記錄 true
console.log(helloFunction === Person.prototype.sayHello);

// 會記錄 "Hello, I'm Alice"
helloFunction.call(person1);

如範例中所示,我們讓所有的參考均指向 sayHello 函數 — 一個在 person1、一個在 Person.prototype、另一個在 helloFunction 變數 — 這些均參考相同的函數。在呼叫的過程中 this 的值會根據我們如何呼叫來決定,最常見的地方是:我們取得函數的物件屬性所在,在表示法中呼叫 this — person1.sayHello()— 會設定 this 為我們取得函數的地方 (person1),這也是 person1.sayHello() 顯示的名稱為 "Alice" 以及 person2.sayHello() 顯示的名稱為 "Bob" 的原因。但如果我們以其他的方式來呼叫,那麼 this 結果將截然不同:在變數中呼叫 this — helloFunction()— 會設定 this 為所在的全域物件 (在瀏覽器即為 window)。由於物件 (可能) 並沒有 firstName 屬性,因此會得到 "Hello, I'm undefined" 這樣的結果 (在 Loose 模式才有這樣的結果,若在 strict mode 則會不同 [會出現錯誤],為了避免混淆,此處將不會再詳述)。 或者我們可以像最後一個例子使用 call (或 apply) 來明確的設定 this。

注意:請參考 call 及 apply 來取得更多有關 this 的資訊

繼承

繼承是一種用一個或多個類別建立一個特殊版本類別的方式 (JavaScript 僅支援單一繼承)。這個特殊的類別通常稱做子類別,而其引用的類別則通常稱作父類別。在 JavaScript 您可以指定父類別的實例到子類別來做到這件事。在最近的瀏覽器中您也可以使用 Object.create 來實作繼承。

注意:JavaScript 不會偵測子類別的 prototype.constructor (請參考 Object.prototype),所以我們必須手動處理。請參考在 Stackoverflow 的問題 "為什麼一定要設定 prototype 的建構子?"。

於以下範例,我們會定義類別 Student 做為 Person 的子類別。然後我們會重新定義 sayHello() 方法然後加入 sayGoodBye() 方法。

// 定義 Person 建構子
var Person = function(firstName) {
  this.firstName = firstName;
};

// 加入兩個方法到 Person.prototype
Person.prototype.walk = function(){
  console.log("I am walking!");
};

Person.prototype.sayHello = function(){
  console.log("Hello, I'm " + this.firstName);
};

// 定義 Student 建構子
function Student(firstName, subject) {
  // Call the parent constructor, making sure (using call)
  // that "this" is set correctly during the call
  Person.call(this, firstName);

  // Initialize our Student-specific properties
  this.subject = subject;
}

// 建立 Student.prototype 物件來繼承 Person.prototype。
// 注意: 在此處經常見的錯誤是使用 "new Person()" 來建立
// Student.prototype。不正確的原因許多個,尤其是
// 我們沒有給予 Person 任何 "firstName" 的參數。
// 呼叫 Person 的正確位置在上方,也就是我們呼叫 Student 
// 的地方。
Student.prototype = Object.create(Person.prototype); // 詳見以下說明

// 設定 "constructor" 屬性參考 Student
Student.prototype.constructor = Student;

// 替換 "sayHello" 方法
Student.prototype.sayHello = function(){
  console.log("Hello, I'm " + this.firstName + ". I'm studying "
              + this.subject + ".");
};

// 加入"sayGoodBye" 方法
Student.prototype.sayGoodBye = function(){
  console.log("Goodbye!");
};

// 範例用法:
var student1 = new Student("Janet", "Applied Physics");
student1.sayHello();   // "Hello, I'm Janet. I'm studying Applied Physics."
student1.walk();       // "I am walking!"
student1.sayGoodBye(); // "Goodbye!"

// 檢查 instanceof 可正常運作
console.log(student1 instanceof Person);  // true 
console.log(student1 instanceof Student); // true

Student.prototype = Object.create(Person.prototype); 一行:在舊版的 JavaScript 引擎沒有 Object.create,可以使用 "polyfill" (又稱 "shim",請參考以下文章連結) 或使用函數來達到同樣的效果,如:

function createObject(proto) {
    function ctor() { }
    ctor.prototype = proto;
    return new ctor();
}

// 用法:
Student.prototype = createObject(Person.prototype);
注意: 請參考 Object.create 來了解其功能與舊引擎使用的 shim。

要在不管物件如何實例化的情況下確保 this 指向正確的地方並不簡單,儘管如此,這裡仍有一個簡單的習慣用法讓這件事變的較簡單。

var Person = function(firstName) {
  if (this instanceof Person) {
    this.firstName = firstName;
  } else {
    return new Person(firstName);
  }
}

封裝

於上述例子中 Student 並不需要知道 Person 類別的 walk() 方法實作的方式,但仍可以使用該方法,除非我們想要更改該函數,否則 Student 類別並不需要明確的定義該函數。這樣的概念稱就叫作封裝 (Encapsulation),透過將每個類別的資料與方法包裝成一個單位。

隱藏資訊在其他語言是常見的功能,通當會使用私有 (Private) 與保護 (Protected) 方法/屬性。既使如此,您仍可在 JavaScript 模擬類似的操作,這並不是物件導向程式設計必要的功能。3

抽象化

抽象化是一個機制能讓您將工作問題的目前部份進行建立模型,不論是用繼承 (特殊化) 或是組合的方式。JavaScript 可以透過繼承達到特殊化 (Specialization),並可讓類別實例成為其他物件的屬性來達到組合 (Composition)。

JavaScript 的 Function 類別繼承 Object 類別 (這示範了模型的特殊化) 而 Function.prototype 屬性是 Object 的實例 (這示範了組合)。

var foo = function () {};

// 會記錄 "foo is a Function: true"
console.log('foo is a Function: ' + (foo instanceof Function));

// 會記錄 "foo.prototype is an Object: true"
console.log('foo.prototype is an Object: ' + (foo.prototype instanceof Object));

多型

如同所有方法與屬性會定義在 prototype 之中,不同的類別可以定義相同名稱的方法,而這些方法的有效範圍其所在的類別之中,除非兩個類別之間有父子關係 (例如,其中一個類別是繼承另一個類別而來)。

注意

在 JavaScript 並不是只有這些方式可以實作物件導向程式設計,JavaScript 在這方面非常有彈性。另外,在此處示範的方法並沒有使用任何語言特殊技巧,也沒有模仿其他語言的物件理論來實作。

在 JavaScript 也有其他的方式可以做更進階的物件導向程式設計,但已超出本篇簡介的範圍。

參考文獻

  1. Wikipedia - Object-oriented programming
  2. Wikipedia - Prototype-based programming
  3. Wikipedia - Encapsulation (object-oriented programming)

相關資料

文件標籤與貢獻者

 最近更新: cwlin0416,