物件導向(Object oriented programming)是一種寫程式的典範(paradigm),程式典範指的是一種程式碼風格以及如何組織程式碼。
一般而言,物件導向有四大原則,分別是:
- Abstraction 抽象:不用知道細節,只需要知道功能;
- Encapsulation 封裝:主要是用來避免私有(private)變數或方法意外從外部改動;
- Inheritance 繼承
- Polymorphism 多型:多型的原意是多種形狀,意思是父類別可被多種不同型態的子類別繼承;此外,子類別可覆寫(overwrite, override)自父類別繼承的方法。
OOP in JS
JavaScript的物件導向並沒有完整遵循上述四個原則,譬如JavaScript截至目前為止並沒有私有方法可以封裝,因此和一般程式語言的物件導向並不太ㄧ樣。
此外,一般的程式語言是以"複製"的方式繼承父類別方法,而JavaScript則是透過「原型(prototype)」和「原型鏈(prototype chain)」繼承方法。
嚴格來說JavaScript目前沒有跟其他語言一樣的類別(Class)與法,現代JavaScript的Class只是類似其他程式語言「類別」的語法糖,用來包裝constructor function;constructor function是在JavaScript ES6 Class語法糖還沒出來以前的一種特殊函式,可用來大量建立物件 ─ 也就是說,constructor function的作用 "類似" 其他程式語言的類別。
Prototype本身是JavaScript用來讓物件繼承或委託共用方法的特殊屬性,constructor function也擁有這一個屬性;用prototype建立出來的物件會自動讓讓物件"繼承"prototype擁有的方法,不過物件要藉由屬性物件 __proto__
繼承並存取prototype的方法。
因為JavaScript並不是以複製的方式繼承方法,因此有時候也可稱JavaScript的繼承行為是「委託(delegation)」。
至於要怎麼實作JavaScript的物件導向,可大致分為三種:
- Object.create
- Constructor Function
- ES6 Class
為方便起見,以下會稱constructor function為constructor(建構式),而constructor建立的物件稱為instance(實例)
Object.create
Object.create實作OOP的方式是先建立一個prototype:
const UserProto = {
init(account, password){
this.account = account;
this.password = password;
}
showUser(){
console.log(this.account);
}
}
const jack = Object.create(UserProto);
jack.init('jack123', 123);
範例中的 Object.create 會回傳一個空物件 {}
給jack,然後我們利用物件繼承的prototype存取 init()
函式替物件jack新增屬性。
Object.create建立物件的過程並沒有constructor、new或class等關鍵字,由此應該就能一目了然,JavaScript物件本質上是透過prototype來繼承/委託共用函式,而非constructor。
雖然,Object.create是三個方法中比較不常使用到的方法,但Object.create可以用來建立constructor和物件的繼承關係。
Constructor Function
Constructor function其實是一般的函式,但可用 new
關鍵字大量建立物件,作用類似於其他程式語言的class:
const User = function(account, password){
this.account = account;
this.password = password;
// 千萬別在這裡寫函式
// this.showAccount = function(){
// console.log(this.account);
// }
}
const jack = new User('jack123', 123);
// 建立共用函式
User.prototype.showUser = function(){
console.log(this.account);
}
使用 new
關鍵字呼叫函式會發生以下事情:
- 先創建一個空物件
{}
; - 接著函式會被呼叫,並且創造
this
指向步驟一創造的物件; - 再來這個物件創造
__proto__
屬性物件並鏈結至constructor function的prototype,在這個例子就是User.prototype
; - 最後cunstructor function會自動回傳這個物件,在這個例子裡面物件被儲存在
jack
。
要注意千萬別把函式寫在constructor裡面,會導致每個instance都會擁有自己的"相同函式",就如同範例中註解掉的 showAccount
,instances並不會共用這個函式,而是每建立一個instance都會留一塊記憶體創造一個屬於這個instance自己的 showAccount
函式,如果有上百個instances就會有上百個看起來長得一樣卻分屬於不同記憶體的 showAccount
,非常浪費空間。
替instances建立共用函式的方式是在construtro的prototype新增函式,也就是在 User.prototype
新增方法,讓instances去繼承這個方法,如同範例中的 User.prototype.showUser
。
可以用幾個方式來檢查物件是否是從某個constructor function所建立出來的,也就是constructor function的instance;也能檢查物件的 __proto__
屬性是否為 constructor function的 prototype
:
// 檢查物件是否為User的instance
console.log(jack instanceof User); // true
// 檢查物件的__proto__屬性是否為User的prototype
console.log(jack.__proto__ === User.prototype); // true
// 或者
console.log(User.prototype.isPrototypeOf(jack)); // true
ES6 Class
最後一個是ES2015才出來的語法糖,前面介紹constructor function有說到instances的共用函式(方法)必須另外寫在 User.prototype
,如果有多個共用函式就必須在constructor function以外的地方寫多個方法讓instances可以一起繼承,但這樣的寫法會降低程式碼可讀性,而class語法的好處是可以將instance屬性和方法寫在一起,以下用class宣告式的寫法示範:
class User {
constructor(){
this.account = account;
this.password = password;
}
showUser(){
console.log(this.account);
}
}
Class內的 showUser
函式會自動以 prototype
的方式讓instance繼承,也就相當於前面所寫的 User.prototype.showUser
。
這篇只有簡單整理物件導向的四大原則、JavaScript的物件導向語法及instance的屬性和方法,關於物件導向還有子類別繼承、靜態屬性(static property)與靜態方法(static method)幾個重要主題,為了版面簡潔就留待後幾篇整理。
References
The Complete JavaScript Course 2023: From Zero to Expert!