Object-oriented to the core, JavaScript features powerful, flexible OOP capabilities. This article starts with an introduction to object-oriented programming, then reviews the JavaScript object model, and finally demonstrates concepts of object-oriented programming in JavaScript. This article does not describe the newer syntax for object-oriented programming in ECMAScript 6.
複習 JavaScript
若您對 JavaScript 變數、型態、函數及作用範圍的觀念並不是很有信心,您可以閱讀 A re-introduction to JavaScript 中相關的主題。您也可以參考 JavaScript Guide。
物件導向程式設計
物件導向程式設計 (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 Reference: Standard built-in objects 來取得在 JavaScript 中所有核心物件的清單。
每個在 JavaScript 中的物件均為物件 Object
的
實例 (Instance),因此會繼承其所有的屬性與方法。
自訂物件
類別 (Class)
JavaScript 是以雛形為基礎的程式語言,並沒有像在 C++ 或 Java 中看以到的 class
敘述句,這有時會讓習慣使用有 class
敘述句的程式設計師混淆。JavaScript 使用函數來作為類別 (Class) 的建構子 (Constructor),要定義一個類別 (Class) 如同定義一個函數 (Function)一樣簡單,以下例子中我們使用空的建構子來定義了一個新的 Person 類別。
var Person = function () {};
物件 (Object) - 類別的實例
要建立物件 obj
的新實例,我們可以使用 new obj
敘述句,並將結果 (型態為 obj
) 指派 (Assign) 到一個變數方便之後存取。
在先前翻例中我們定義了一個名稱為 Person
的類別。在以下的例子我們會建立兩個實例 (person1
與 person2
)。
var person1 = new Person(); var person2 = new Person();
Please see Object.create()
for a new, additional, instantiation method that creates an uninitialized instance.
建構子 (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)
方法即為函數 (也如同函數般定義),但其餘部份是依照屬性的邏輯來運作。呼叫一個方法如同存取一個屬性,但您需要在函數名稱後加上 ()
,有可能會有參數。要定義一個方法,則只需在類別的 prototype
屬性將函數指定 (Assign) 給一個已命名的屬性,接著,您便可用剛指定給屬性的名稱來呼叫該物件的方法。
以下範例中,我們為 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 中,方法是一般的函數物件 (Regular 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。
繼承
繼承是一種用一個或多個類別建立一個特殊版本類別的方式 (JavaScript 僅支援單一繼承)。這個特殊的類別通常稱做子類別,而其引用的類別則通常稱作父類別。在 JavaScript 您可以指定父類別的實例到子類別來做到這件事。在最近的瀏覽器中您也可以使用 Object.create 來實作繼承。
注意:JavaScript 不會偵測子類別的 prototype.constructor
(請參考 Object.prototype),所以我們必須手動處理。請參考在 Stackoverflow 的問題 "Why is it necessary to set the prototype constructor?"。
於以下範例,我們會定義類別 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);
要在不管物件如何實例化的情況下確保 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
抽象化
Abstraction is a mechanism that allows you to model the current part of the working problem, either by inheritance (specialization) or composition. JavaScript achieves specialization by inheritance, and composition by letting class instances be the values of other objects' attributes.
The JavaScript Function class inherits from the Object class (this demonstrates specialization of the model) and the Function.prototype
property is an instance of Object
(this demonstrates composition).
var foo = function () {}; // logs "foo is a Function: true" console.log('foo is a Function: ' + (foo instanceof Function)); // logs "foo.prototype is an Object: true" console.log('foo.prototype is an Object: ' + (foo.prototype instanceof Object));
多型
Just as all methods and properties are defined inside the prototype property, different classes can define methods with the same name; methods are scoped to the class in which they're defined, unless the two classes hold a parent-child relation (i.e. one inherits from the other in a chain of inheritance).
注意
These are not the only ways you can implement OOP in JavaScript, which is very flexible in this regard. Likewise, the techniques shown here do not use any language hacks, nor do they mimic other languages' object theory implementations.
There are other techniques that make even more advanced OOP possible in JavaScript, but those are beyond the scope of this introductory article.
參考文獻
- Wikipedia - Object-oriented programming
- Wikipedia - Prototype-based programming
- Wikipedia - Encapsulation (object-oriented programming)