物件導向 OOP 以 JavaScript 為例
📝 TL;DR
- OOP 四大支柱:封裝(隱藏實作)、繼承(extends)、多型(同名方法不同行為)、抽象(模板類別)。
- JavaScript 實作:class + constructor + private fields (#) + super() + extends。
- 實務:Animal 抽象類 → Dog/Cat 繼承 → makeSound() 多型 → #weight 封裝。
前置知識
- class 語法與 constructor
- this 關鍵字綁定
- 繼承與原型鏈基礎
創建 Class
js
// 宣告類別
class Animal {
constructor(name, age) {
this.name = name;
this.age = age;
}
getInfo() {
return `Name: ${this.name}, Age: ${this.age}`;
}
}使用 Class
使用new關鍵字並傳入建立的動物對象的給定名稱和年齡。有一個 getInfo 方法,用於傳回有關物件的信息
js
// 使用類別
const firstAnimal = new Animal("Rex", 2);
console.log(firstAnimal); // Animal { name: 'Rex', age: 2 }
console.log(firstAnimal.getInfo()); // Name: Rex, Age: 2
const secondAnimal = new Animal("Barney", 5);
console.log(secondAnimal); // Animal { name: 'Barney', age: 5 }
console.log(secondAnimal.getInfo()); // Name: Barney, Age: 5繼承
繼承是類別的一種功能,它使一些類別能夠獲取另一個類別(父類別)的所有方法和屬性,並可以透過添加更多內容來擴展父類。
js
// 宣告類別
class Dog extends Animal {
constructor(name, age, breed) {
super(name, age);
this.breed = breed;
}
bark() {
return "woof";
}
}
class Cat extends Animal {
constructor(name, age, weight) {
super(name, age);
this.weight = weight
}
}
// 使用類別
const myDog = new Dog("Buddy", 2, "Golden Retriever");
console.log(myDog.getInfo()); // Name: Buddy, Age: 2
console.log(myDog.bark()); // woof
const myCat = new Cat("Fluffy", 3, "5kg");
console.log(myCat.getInfo()); // Name: Fluffy, Age: 3封裝
封裝是一種限制機制,使得訪問不使用特殊方法不可能獲得的數據致力於此。在下面的例子中我們標記了權重作為私有財產,為了獲取和設定一個值,我們需要使用 getter 和 setter 方法
js
// 宣告類別
class Cat extends Animal {
#weight; // ------------> 創造私有值
constructor(name, age, weight) {
super(name, age);
this.#weight = weight;
this.weight = weight;
}
getWeight() { // ------------> getter
return this.#weight;
}
setWeight(weight) { // ------------> setter
this.#weight = weight;
}
}
// 使用類別
const myCat = new Cat("Fluffy", 3, "5kg");
console.log(myCat.getWeight()); // 5kg
myCat.setWeight("6kg");
console.log(myCat.getWeight()); // 6kg多型
多型性是利用繼承來重複使用的概念,根據類別的不同,多次使用不同行為的方法類型。
為了理解這一點,讓我們來看看我們的例子——在狗類中我們將刪除bark()方法,並在動物類中添加一個makeSound方法將被貓和狗類覆蓋
js
// 宣告類別
class Animal {
constructor(name, age) {
this.name = name;
this.age = age;
}
makeSound() {
return "Some nice sound made";
}
}
class Dog extends Animal {
constructor(name, age, breed) {
super(name, age);
this.breed = breed;
}
makeSound() {
return "woof";
}
}
class Cat extends Animal {
constructor(name, age, weight) {
super(name, age);
this.weight = weight;
}
makeSound() {
return "meow";
}
}
// 使用類別
const myDog = new Dog("Buddy", 2, "Golden Retriever");
console.log(myDog.makeSound()); // woof
const myCat = new Cat("Fluffy", 3, "5kg");
console.log(myCat.makeSound()); // meow抽象類別
抽象類別是不能被實例化的類,需要從特定繼承的子類,抽象類別提供實作。
我們將Animal類別變更為抽象類別。它不會可以再建立此類別的實例將makeSound標記為抽象方法 - 為了使用它,子類別必須聲明該方法自己的實現
簡單來說,變成抽象後就不能new Animal了,要用Cat去繼承Animal。
js
class Animal {
constructor(name, age) {
this.name = name;
this.age = age;
if (this.constructor === Animal) {
throw new Error("無法建立抽象類別的實例");
}
}
makeSound() {
throw new Error("必須實作 makeSound 方法");
return "Some nice sound made";
}
}
class Dog extends Animal {
constructor(name, age, breed) {
super(name, age);
this.breed = breed;
}
makeSound() {
return "woof";
}
}
class Cat extends Animal {
constructor(name, age, weight) {
super(name, age);
this.weight = weight;
}
// makeSound() {
// return "meow";
// }
makeSound() {
return "meow";
}
}
// const firstAnimal = new Animal("Rex", 2); // Error: 無法建立抽象類別的實例
const myDog = new Dog("Buddy", 2, "Golden Retriever");
const myCat = new Cat("Fluffy", 3, "5kg");
// console.log(myCat.makeSound()); // Error: 必須實作 makeSound 方法類別圖
實戰練習
練習 1:創建 Vehicle 類別(簡單)⭐
建立 Vehicle 類別(brand, model)與 getDetails() 方法,再建立 Car 子類別(doors)。
💡 參考答案
javascript
class Vehicle {
constructor(brand, model) {
this.brand = brand
this.model = model
}
getDetails() {
return `${this.brand} ${this.model}`
}
}
class Car extends Vehicle {
constructor(brand, model, doors) {
super(brand, model)
this.doors = doors
}
}
const myCar = new Car('Toyota', 'Corolla', 4)
console.log(myCar.getDetails()) // Toyota Corolla練習 2:Private field 封裝(簡單)⭐
為 BankAccount 類別建立私有的 #balance,用 deposit() 和 getBalance() 操作。
💡 參考答案
javascript
class BankAccount {
#balance = 0
deposit(amount) {
this.#balance += amount
}
getBalance() {
return this.#balance
}
}
const account = new BankAccount()
account.deposit(1000)
console.log(account.getBalance()) // 1000練習 3:多型與抽象(中等)⭐⭐
建立 Shape 抽象類別(calculateArea),再實作 Circle 與 Rectangle 子類別。
💡 參考答案與提示
javascript
class Shape {
constructor() {
if (this.constructor === Shape) {
throw new Error('無法實例化抽象類別')
}
}
calculateArea() {
throw new Error('必須實作 calculateArea')
}
}
class Circle extends Shape {
constructor(radius) {
super()
this.radius = radius
}
calculateArea() {
return Math.PI * this.radius ** 2
}
}
class Rectangle extends Shape {
constructor(width, height) {
super()
this.width = width
this.height = height
}
calculateArea() {
return this.width * this.height
}
}
const circle = new Circle(5)
const rect = new Rectangle(4, 6)
console.log(circle.calculateArea()) // 78.54
console.log(rect.calculateArea()) // 24延伸閱讀
FAQ
- Q: private field (#) 與 closure 差在哪?
- A: # 是語言級別的私有,closure 是作用域封閉;# 可在類別方法間共享,closure 不行。
- Q: 為何要抽象類別?
- A: 強制子類別實作特定方法,確保介面一致性;避免實例化模板類別。
- Q: 多型的實際用途?
- A: 統一介面、不同實作;如 Array/NodeList 都有 forEach,但內部行為不同。