Minghsin University of Science and Technology
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try a Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

1

Local AI

--

Uedu Code

--

明新科大 AQI 70 24°C PM2.5 19
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 28

物件導向:class

類別、建構函式、方法、getter 與繼承
Interaction實作 20 minute(s)

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 擋下。

想一想:類別與物件

1.

下面的程式會印出什麼?

class Counter {
  constructor() {
    this.count = 0;
  }
  add() {
    this.count++;
    return this;
  }
}
const c = new Counter();
c.add().add();
console.log(c.count, typeof Counter);
2.

下面的程式執行結果是什麼?

class Person {
  constructor(name) {
    this.name = name;
  }
}
const p = Person('Amy');
console.log(p);
3.

關於 JavaScript 的 class,下列哪一個說法正確?

第 1 次作答

想一想:static、getter 與繼承

1.

下面的程式會印出什麼?

class MathTool {
  static square(x) {
    return x * x;
  }
}
const m = new MathTool();
console.log(MathTool.square(3), typeof m.square);
2.

下面的程式會印出什麼?

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);
3.

下面的程式會印出什麼?

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());
4.

類別裡宣告了私有欄位 #balance,在類別外面寫 console.log(acc.#balance) 會怎樣?

第 1 次作答

實作:長方形與正方形

替 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.方法() 呼叫父類別的版本。
  • # 開頭的私有欄位只有類別裡的程式碼能存取,用來保護資料。
這一課完成了!
Next lesson:模組與建置工具
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。