類別語法

August 22, 2022

JavaScript 是基於原型的物件導向,這是個極具彈性的典範,然而每個開發者都有辦法掌握強大的彈性,對於熟悉基於類別的物件導向開發者而言,基於原型也不是他們熟悉的方式,因而在 ES6 之前,模擬類別的需求一直都在,只是各自有不同的模擬方式與風格,這就造成了不同風格間要互動合作時的不便。

話先說在前頭,雖然 ES6 開始提供類別語法,不過嚴格來說,仍是在模擬基於類別的物件導向,本質上 JavaScript 仍是基於原型的物件導向,ES6 的類別語法,主要是提供標準化的類別模擬方式,透過語法蜜糖令程式碼變得簡潔一些,然而也不只是語法糖,其透過一些原生機制,令特性在定義方法或私有性時,更符合 JavaScript 本身的規範。

定義類別

直接來看看如何定義一個類別:

class Account {
    constructor(name, number, balance) {
        this.name = name;
        this.number = number;
        this.balance = balance;
    }

    withdraw(money) {
        if(money > this.balance) {
            console.log('餘額不足');
        }
        this.balance -= money;
    }

    deposit(money) {
        if(money < 0) {
            console.log('存款金額不得為負');
        }
        else {
            this.balance += money;
        }
    }

    toString() {
        return `(${this.name}, ${this.number}, ${this.balance})`;
    }
}

let acct = new Account('Justin Lin', '123-4567', 1000);
for(let p in acct) {
    console.log(`${p}: ${acct[p]}`);
}

ES6 以後使用 class 關鍵字來定義類別,而 constructor 用來定義實例的初始流程,如果類別中沒有撰寫 constructor,也會自動加入一個無參數的 constructor() {}constructor 最後隱含地傳回物件本身,也就是 this,如果在 constructor 明確地 return 某個物件,那麼 new 的結果就會是該物件。

在定義方法時,方式與〈物件實字簡化與增強〉談到的物件實字定義語法相同;namenumberbalance 都是公開可見。

私有語法

ES13 以後,定義類別時可以在類別本體直接定義值域(Field),每個類別的實例,會各自擁有值域,也就是物件的特性,例如,若要記錄 Account 建立時間,ES13 以後可以如下:

class Account {
    creationTimeMillis = new Date().getTime(); // 定義值域
    
    constructor(name, number, balance) {
        this.name = name;
        this.number = number;
        this.balance = balance;
    }
    
    ...

    debug() {
        return `${this.creationTimeMillis}: (${this.name}, ${this.number}, ${this.balance})`; 
    }
}

let acct = new Account('Justin Lin', '123-4567', 1000);
console.log(acct.debug());

如果值域沒有指定值,預設會是 undefined,在內部要存取值域,同樣也是透過 this,如果想定義私有成員,可以為值域加上 #,私有值域在外部無法存取,在類別內部,可以透過 this.#xxxx 的方式存取,例如:

class Account {
    #creationTimeMillis = new Date().getTime();  // 定義私有值域
    
    constructor(name, number, balance) {
        this.name = name;
        this.number = number;
        this.balance = balance;
    }

    ...
    
    debug() {
        return `${this.#creationTimeMillis}: (${this.name}, ${this.number}, ${this.balance})`; 
    }
}

let acct = new Account('Justin Lin', '123-4567', 1000);
console.log(acct.debug());

如果想測試物件是不是有私有成員,可以使用 #xxx in obj 的語法;# 也可以加在方法前,代表私有方法,只能在類別內部呼叫,呼叫時是透過 this.#mmm(..) 的方式,如果在類別內,想檢查自身是否有某個私有特性的話,可以使用 #creationTimeMillis in this 這種語法。

既然使用了類別語法,通常就是希望以基於類別的物件導向來思考,不過,範例中的 Account 本身,確實仍是 Function 的實例,withdraw 方法則是定義在 Account.prototype 的特性,預設為不可列舉,Account.prototype.constructor 參考的就是 Account,這些與 ES5 自定建構式、方法時的相關設定相同,使用類別語法來做,著實省了不少功夫。

既然本質上還是基於原型,這表示還是可以對 Account.prototype 直接添加特性,之後 Account 的實例也能找得到該特性;也可以直接將 withdraw 參考的函式指定給其他變數,或者是指定為另一物件的特性,透過該物件來呼叫函式,該函式的 this 一樣是依呼叫者而決定;每個透過 new Account(...) 建構出來的實例,本身的原型也都是參考至 Account.prototype

然而不同的是,使用 class 定義的Account只能使用 new 來建立實例,直接以函式的呼叫方式,像是 Account(...)Account.call(...)Account.apply(...) 都會發生 TypeError

類別也可以使用運算式的方式來建立,可以是匿名類別,必要時也可以給予名稱:

> let clz = class {
...     constructor(name) { this.name = name; }
... }
undefined
> new clz('xyz')
clz { name: 'xyz' }
> var clz2 = class Xyz {
...     constructor(name) { this.name = name; }
... }
undefined
> new clz2('xyz')
Xyz { name: 'xyz' }
>

static 成員

類別的方法若加上 static,那麼該方法會是個靜態方法,也就是以類別為名稱空間的一個函式:

class Circle {
    static toRadians(angle) {
        return angle / 180 * Math.PI;
    }
}

ES13 之前,可以在 static 後置 getset,若想模擬靜態特性的話可以使用,例如,如下定義之後,可以使用 Circle.PI 來取得 3.14159:

class Circle {
    ...
    static get PI() {
        return 3.14159;
    }
}

在類別的 static 方法中若出現 this,代表的是類別本身。例如:

> class Foo {
...     static get self() {
.....         return this;
.....   }
... }
undefined
> Foo.self
[Function: Foo]
>

ES13 以後,可以直接建立 static 特性,因此在 static 方法裡存取 static 值域的方式可以是:

class Circle {
    static PI = 3.14159;

    static area(radius) {
        return radius * radius * this.PI;
    }
}

私有語法 # 也可以套用在 static 方法或成員上。

static 初始區塊

ES13 以後,類別裡可以定義 static 初始區塊,每個類別被定義完成時執行一次:

> class C {
...     static {
.....         console.log('XD');
.....   }
... }
XD
undefined
>

分享到 LinkedIn 分享到 Facebook 分享到 Twitter