Learning Objectives
- 說明類別與物件的關係,以及 JavaScript 的 class 是原型式物件導向的語法糖
- 用 class、constructor 定義類別,用 new 建立物件,並知道 class 必須用 new 呼叫、宣告前不能使用
- 定義方法、getter/setter 與 static 靜態成員,分辨哪些屬於實例、哪些屬於類別
- 用 extends 與 super 繼承父類別,並用私有欄位 # 保護資料
類別與物件
類別(class)像一張設計藍圖,物件(object,也叫實例 instance)是照著藍圖做出來的東西。 以汽車工廠來比喻:類別是汽車的設計圖,物件是實際生產出來的每一輛車。每一輛車的顏色、里程數(狀態)都不一樣, 但結構和能做的事(行為)都相同。
物件導向語言有兩種做法:
| 做法 | 代表語言 | 物件從哪裡來 |
|---|---|---|
| 以類別為基礎(class-based) | Java、C++ | 先定義類別,再由類別產生物件 |
| 以原型為基礎(prototype-based) | JavaScript | 物件直接繼承另一個物件(上一課的原型鏈) |
ES2015 為 JavaScript 加入了 class 語法,寫起來很像 Java,但底層仍然是 prototype:class 是原型式物件導向的語法糖。
方法會放在 類別.prototype 上,用 new 建立的物件透過原型鏈找到它們。
定義類別:constructor 與方法
class Person {
constructor(name, age) { // 用 new 建立物件時呼叫一次
this.name = name; // this 是正在建立的物件
this.age = age;
}
greet() { // 方法:不用寫 function,方法之間不加逗號
return '我是 ' + this.name;
}
}
const amy = new Person('Amy', 20);
和上一課的建構函式寫法對照:
| 建構函式+prototype | class |
|---|---|
function Person(name) { this.name = name; } |
class Person { constructor(name) { this.name = name; } } |
Person.prototype.greet = function () { … }; |
在 class 本體裡寫 greet() { … } |
忘了 new 不會報錯,this 指錯地方 |
忘了 new 直接 TypeError |
一個類別只能有一個 constructor,寫兩個是 SyntaxError;不需要初始化時可以省略,JavaScript 會自動提供一個空的。
建立一個 Person 物件
播放到第 4 行時看右邊:框架名稱是 Person.constructor,多了一列 this,就是 new 剛建立的物件。右邊的 amy 會顯示 [[Prototype]]: Person.prototype。最後兩行證明 class 仍是 prototype:typeof Person 是 function,greet 就在 Person.prototype 上。
兩條規則與類別運算式
- 一定要用
new:Person('Amy')直接丟出TypeError: Class constructor Person cannot be invoked without 'new'。 - 宣告之前不能使用:類別宣告和
let一樣,在宣告那一行之前存取會得到ReferenceError(投影片說「不會 hoisting」,指的就是這件事)。function宣告則可以在宣告前呼叫。
類別也可以寫成運算式,指定給一個變數:
const Shape = class Polygon {}; // 具名的類別運算式
console.log(Shape.name); // 'Polygon':類別名稱用 name 屬性取得
const Point = class {}; // 不具名的類別運算式,name 是 'Point'
在宣告前使用、忘了 new
兩個錯誤都用 try…catch 接住,印出錯誤的名稱與訊息。注意第一個錯誤發生在第 3 行,這時 Cat 還沒有宣告;第二個錯誤是忘了寫 new。
屬性、getter/setter 與 static
屬性有兩種寫法:在 constructor 裡用 this.x = …,或直接寫在類別本體裡的類別欄位(class fields,ES2022):
class Counter {
count = 0; // 每個實例都有自己的 count,初始值 0
static total = 0; // 靜態欄位:屬於類別本身
}
getter 與 setter 讓「讀取」與「設定」屬性時執行一段程式,外面用起來像一般屬性(不用加括號):
get fahrenheit() { return this.celsius * 9 / 5 + 32; } // 讀 t.fahrenheit 時執行
set fahrenheit(f) { this.celsius = (f - 32) * 5 / 9; } // t.fahrenheit = 50 時執行
同一個名稱只能有一個 getter、一個 setter;setter 一定剛好有一個參數。只有 getter 沒有 setter 時,這個屬性就是唯讀的。
static:加上 static 的方法或欄位屬於類別本身,用 類別名稱.方法() 呼叫,不需要先建立物件;
實例不能呼叫靜態方法(t.fromFahrenheit 是 undefined)。常用來寫工具函式或「另一種建立物件的方式」。
溫度換算:getter、setter 與 static
物件身上只有 celsius 一個屬性,fahrenheit 是算出來的:讀取時呼叫 getter(第 10 行),設定時呼叫 setter(第 14 行)。注意框架名稱:Temperature.get fahrenheit、Temperature.set fahrenheit。靜態方法 fromFahrenheit 的 this 是類別本身。
繼承:extends 與 super
用 extends 讓子類別繼承父類別的屬性與方法,再加上自己的東西:
class Cat extends Animal {
constructor(name, color) {
super(name); // 先呼叫父類別的 constructor
this.color = color; // 再加上子類別自己的屬性
}
speak() { // 覆寫(override)父類別的方法
return super.speak() + ':喵~'; // super.方法() 呼叫父類別的版本
}
}
- 子類別寫了 constructor,就必須先呼叫
super(…)才能使用this,否則是ReferenceError - 子類別沒有寫 constructor 時,會自動呼叫
super(...傳進來的參數) - 底層仍然是原型鏈:
Cat.prototype的原型是Animal.prototype,所以mimi instanceof Animal也是true
Cat 繼承 Animal
第 13 行的 super(name) 會進入 Animal.constructor:右邊可以看到兩個 constructor 框架疊在一起,this 是同一個物件。展開 mimi 的 [[Prototype]]:Cat.prototype 再往上是 Animal.prototype。
私有欄位 #:外面拿不到的資料
名稱以 # 開頭的欄位是私有欄位(private field,ES2022):只有類別本體裡的程式碼能讀寫。
在類別外面寫 acc.#balance,整份程式在執行之前就會出現 SyntaxError。
class BankAccount {
#balance = 0; // 私有欄位要先在類別本體裡宣告
deposit(amount) { this.#balance += amount; }
get balance() { return this.#balance; } // 只開放讀取
}
這和第 12 課用閉包把資料藏起來的目的相同:外面只能透過你開放的方法操作資料,方法裡可以先檢查金額是否合理。
方法也可以是私有的(#check() { … })。早期的程式常用底線開頭(this._balance)表示「請不要直接改」,但那只是慣例,外面還是改得到。
用私有欄位保護存款餘額
看右邊的 acc:只看得到 owner,看不到 #balance。逐步執行動畫和類別外面的程式一樣,讀不到私有欄位;Object.keys 也只列出 owner。第 26 行想直接改 balance,但它只有 getter,賦值被忽略;存負數則被 deposit 擋下。
想一想:類別與物件
下面的程式會印出什麼?
class Counter {
constructor() {
this.count = 0;
}
add() {
this.count++;
return this;
}
}
const c = new Counter();
c.add().add();
console.log(c.count, typeof Counter);
下面的程式執行結果是什麼?
class Person {
constructor(name) {
this.name = name;
}
}
const p = Person('Amy');
console.log(p);
關於 JavaScript 的 class,下列哪一個說法正確?
想一想:static、getter 與繼承
下面的程式會印出什麼?
class MathTool {
static square(x) {
return x * x;
}
}
const m = new MathTool();
console.log(MathTool.square(3), typeof m.square);
下面的程式會印出什麼?
class Rect {
constructor(w, h) {
this.w = w;
this.h = h;
}
get area() {
return this.w * this.h;
}
}
const r = new Rect(3, 4);
r.w = 5;
console.log(r.area);
下面的程式會印出什麼?
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return this.name + ':……';
}
}
class Dog extends Animal {
speak() {
return super.speak() + '汪';
}
}
console.log(new Dog('Rex').speak());
類別裡宣告了私有欄位 #balance,在類別外面寫 console.log(acc.#balance) 會怎樣?
實作:長方形與正方形
替 Rectangle 加上 getter area(寬乘以高);再寫 Square 繼承 Rectangle:constructor 只收一個邊長 side,用 super 把它同時當成寬與高;describe() 改成回傳 邊長 5 的正方形。
重點精華回顧
- 類別是藍圖,物件是照藍圖做出的實例;JavaScript 以原型為基礎,ES2015 的
class是原型的語法糖。 class裡的constructor在new時呼叫一次,用this.屬性 = 值設定屬性;一個類別只有一個 constructor。- class 一定要用
new呼叫,宣告之前不能使用;類別也能寫成運算式,名稱用name取得。 - 方法寫在類別本體裡,會放到
類別.prototype,所有實例共用。 - getter/setter(
get、set)讓屬性的讀寫執行程式;static成員屬於類別本身,實例不能呼叫。 extends繼承父類別,子類別的 constructor 要先呼叫super(…);super.方法()呼叫父類別的版本。#開頭的私有欄位只有類別裡的程式碼能存取,用來保護資料。