JavaScript | ES6 中最容易誤會的語法糖 Class - 基本用法
前言
Hi!本來在之前想和 Promise 、 Fetch 一起完成的 ES6 小小三部曲,一直拖到現在才真正動工,其實本來沒有想要另外學了,但因為最近讀 Refactor 發現善用 Class 也是很好的重構技巧之一,所以決定花點時間讓自己熟悉一下。
Class
先來解釋一下標題好了,
為什麼會被誤會?如果各位讀者有寫過其他像是 Java 的 Class-Based 物件導向語言,都會知道下面幾件事情:
- 分成 Class 和 Object 兩種。
- Class 內部會描述 Properties 和 Method。
- Class 能建構出 Object ,也被稱作 Instance 。
- Instance 能夠使用 Class 內的 Method,並藉由 Method 存取 Object 內的資料。
咦?就算沒寫過,但這樣看起來似乎和 JavaScript 的 Constructor 及 Instance 差不多對吧?
對!但是差別在於第一點所說的「分成 Class 和 Object 兩種」
在 JavaScript 的 Prototype-Based 物件導向中,不區分 Class 和 Object 所有的東西都可以是 Object ,
再來,如果在 Java 中要做 Class 間的繼承,得在定義 Class 時指定要繼承的父類別。在 JavaScript 中則是以改變 Constructor 的 Prototype 來使用其他 Constructor 的 Method 。
這些差別都是取決於物件導向是基於 Class 或 Prototype ,因此就算 ES6 新增了一個 Class 保留字用來當 Constructor 創建 Instance ,也不代表它物件導向的方式會變成 Class-Based ,
所以千萬不要搞混囉! Class 只是簡化了 JavaScript 中操作 Constructor 的語法糖而已。
Constructor
Constructor 是建構器,可以用它產生擁有相同 Properties 的 Object ,例如大家都熟悉的:
function Person(name) {
// public properties
this.name = name;
// private value
const state = 'Taiwan';
// privileged methods
this.getFrom = () => `${this.name} from ${state}.`;
}
const john = new Person('John');
console.log(john); // { name: 'John', getFrom: [Function] }
console.log(john.state); // undefined
console.log(john.getFrom()); // John from Taiwan.
Person 內裡面有三行程式碼,來看看它們分別是什麼:
name
是經過Person
創建出來後會帶的 Own Properties (自有特性),會在呼叫 Constructor 時當作參數傳入。state
是一個 Private value (私有值),它只存在於 Constructor 創建 Instance 時另外產生的環境。- 雖然
state
不是 Instance 的 Own Properties ,但是透過getForm
便能夠取得state
的值,這種讀取 Private value 的方式稱作 Privileged Method (特權方法)。
那接著進入 ES6 時期的 Constructor , Class 版本又會是什麼樣子:
class Person {
constructor(name) {
this.name = name;
}
getFrom() {
const state = 'Taiwan';
return `${this.name} from ${state}.`;
}
}
const john = new Person('John');
console.log(JSON.stringify(john)); // { name: 'John' } public 方法不再顯露於物件裡
console.log(JSON.stringify(john.state)); // undefined
console.log(JSON.stringify(john.getFrom())); // John from Taiwan.
有沒有煥然一新的感覺?用 Class 來宣告 Constructor 在語義上面會更清楚,不像之前只能透過字首的大小寫來判斷是否為 Constructor ,且還有可能會有未遵照規則導致使用錯誤的情況發生。
透過 new
呼叫時傳入的參數會由 Class 內的 constructor
給接收,並在同一區塊建立 Public Properties ,而 Method 的部分則是在 constructor
外做描述或存取資料, Private Value 就存放在 Method 中,依然無法從 Instance 中取得。
然後這邊是個很棒的時間,可以讓我們驗證 Class 的操作是否仍然為 Prototype ,如果是透過 Constructor 建立的 Instance ,應該會擁有相同的 Prototype :
Inheritance
繼承的話在 Class 上也變得方便許多,想當初如果要 Constructor 上處理繼承,就得使用 call
在 Constructor 創建 Instance 時來指定 this
呼叫另一個 Constructor,像是這樣子:
function Person(name) {
this.name = name;
const state = 'Taiwan';
this.getFrom = () => `${this.name} from ${state}.`;
}
function Employee(name, position) {
// 將 this 送給 Person 建立 properties
Person.call(this, name);
this.position = position;
// public properties
this.getPosition = () => `${this.name}'s position is a ${this.position}.`;
}
const luck = new Employee('Luck', 'Front-end');
console.log(luck.getFrom()); // Luck from Taiwan.
console.log(luck.getPosition()); // Luck's position is the Front-end.
看起來有些複雜了對吧?但如果是 Class 只需要利用 extends
和 super
便可輕鬆處理 Constructor 間的 Inheritance :
class Person {
constructor(name) {
this.name = name;
}
getFrom() {
const state = 'Taiwan';
return `${this.name} from ${state}.`;
}
}
// 使用 extends 指定 parent class
class Employee extends Person {
constructor(name, position) {
// 用 super 呼叫 extends 指定的 class
super(name);
this.position = position;
}
getPosition() {
return `${this.name}'s position is a ${this.position}.`;
}
}
const luck = new Employee('Luck', 'Front-end');
console.log(luck.getFrom()); // Luck from Taiwan.
console.log(luck.getPosition()); // Luck's position is the Front-end.
上方在定義 Employee
時另外用了 extends
指定了 Person
,這麼一來就等於是繼承了 Person
的 Properties 和 Method ,但為什麼在 Employee
中的 constructor 中還要使用 super
把 name 傳給 Person
呢?
當子類別自身也需要透過 constructor 建立 Properties 時,就需要使用 super
另外指定要送給父類別的值,否則就 Person
來說,創建 Instance 時將兩個值送入 Employee
, Person
根本不曉得哪一個才是要被指定成 name
的資料,這裡大家可以想像成是用 call
來呼叫另一個 Constructor 的感覺。
也就是說了,如果當今天不需要透過 Employee
創建 Properties ,僅僅是增加 Method,那 super 就可以省略,因為所有的參數都會是給 Person
的:
class Person {
constructor(name) {
this.name = name;
}
getFrom() {
const state = 'Taiwan';
return `${this.name} from ${state}.`
}
}
class Employee extends Person {
sayHello() {
return `Hello!I am ${this.name}!`;
}
}
const luck = new Employee('Luck');
console.log(luck.getFrom()); // Luck from Taiwan.
console.log(luck.sayHello()); // Hello!I am Luck!
最後記得那個可怕的 Super Call(超呼叫)嗎?當子類別的 Method 要呼叫父類別的 Method 執行就叫 Super Call ,在未有 Class 時,仍然是需要使用 call
將 this
指定給父類別 Prototype 的 Method 做執行:
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function () {
return `Hello!I am ${this.name}!`;
};
function Employee(name) {
Person.call(this, name);
}
Employee.prototype = Object.create(Person.prototype);
// 進行 Super call
Employee.prototype.superCallSayHello = function () {
return Person.prototype.sayHello.call(this);
};
const luck = new Employee('Luck');
console.log(luck.superCallSayHello()); // Hello!I am Luck!
儘管我已經將例子盡量簡化了,看起來還是很麻煩,且 Person
要被 Super Call 的 Method 也得另外設置在 Prototype 中。
但是到 Class 時代後一切便不同了,答案就在運用上方提到的 super
,既然是透過傳送參數給它來創建 Properties ,那也可以透過 super
直接呼叫父類別中的Method :
class Person {
constructor(name) {
this.name = name;
}
getFrom() {
const state = 'Taiwan';
return `${this.name} from ${state}.`;
}
}
class Employee extends Person {
constructor(name, position) {
super(name);
this.position = position;
}
getPosition() {
return `${this.name}'s position is the ${this.position}.`;
}
// super call
superCallGetForm() {
return super.getFrom();
}
}
const luck = new Employee('Luck', 'Front-end');
console.log(luck.superCallGetForm()); // Luck from Taiwan.
是不是簡潔多了?透過 super
便不需要再手動處理 Prototype 。
那依照慣例,在 Inheritance 這個段落的結尾也來驗證 Class 間的 Inheritance 是否也同樣是在操作 Constructor 的 Prototype ,如果是的話,那子類別 Employee
的 Prototype 應該會等於父類別 Person
,而 Instance 的 Prototype 依然指向 Employee
:
到這裡應該可以清楚明白,
學會在 Class 中創建 Instance 、 Inheritance 、 Super Call 後,接著來看看 Class 提供的 Static Method (靜態方法)!
Static
在 Class 內的 Method 可以加上 static 前綴,使它變成 Static Method (靜態方法),被定義為 Static Method 可以直接以 Constructor 呼叫,但創建出來的 Instance 是無法使用它的:
class Person {
constructor(name) {
this.name = name;
}
static sayHello(name) {
return `Hi!${name}!`;
}
getFrom() {
const state = 'Taiwan';
return `${this.name} from ${state}.`;
}
}
console.log(Person.sayHello('Luck')); // Hi!Luck!
Getter & Setter
前綴詞其實不只有 static
,連存取器的 get
及 set
也可以在 Class 中作定義:
class Person {
constructor(name) {
this.name = name;
}
static sayHello(name) {
return `Hi!${name}!`;
}
get age() {
if (this._age !== undefined) {
return `${this.name} age is ${this._age}.`;
}
return `Don't know ${this.name}'s age.`;
}
set age(age) {
this._age = age;
}
getFrom() {
const state = 'Taiwan';
return `${this.name} from ${state}.`;
}
}
const john = new Person('John');
console.log(john.age); // Don't know John's age.
john.age = 18;
console.log(john.age); // John age is 18.
經常用於不想 Instance 直接存取的狀況,所以利用 Getter 和 Setter 來假裝操作 Properties ,在設定及取值時都先經過一些邏輯判斷再決定怎麼處理。
以上是對於 Class 的一些整理,上半部主要是在比較 Class 出現前後對 Constructor 及 Inheritance 的操作有什麼差別,結尾講解了 Static 和 存取器在 Class 中的使用方式。
如果文章中有任何問題,或是不理解的地方,都可以留言告訴我!謝謝大家!
參考資料