JS: OOP - Introduction

物件導向(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的物件導向,可大致分為三種:

  1. Object.create
  2. Constructor Function
  3. 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 關鍵字呼叫函式會發生以下事情:

  1. 先創建一個空物件 {}
  2. 接著函式會被呼叫,並且創造 this 指向步驟一創造的物件;
  3. 再來這個物件創造 __proto__ 屬性物件並鏈結至constructor function的prototype,在這個例子就是 User.prototype
  4. 最後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!