類別語法
August 22, 2022JavaScript 是基於原型的物件導向,這是個極具彈性的典範,然而每個開發者都有辦法掌握強大的彈性,對於熟悉基於類別的物件導向開發者而言,基於原型也不是他們熟悉的方式,因而在 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
的結果就會是該物件。
在定義方法時,方式與〈物件實字簡化與增強〉談到的物件實字定義語法相同;name
、number
、balance
都是公開可見。
私有語法
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
後置 get
、set
,若想模擬靜態特性的話可以使用,例如,如下定義之後,可以使用 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
>