本文 AI 產出,尚未審核
JavaScript – 原型與繼承:實例方法 vs 原型方法
簡介
在 JavaScript 中,物件的行為(方法)可以直接寫在每個實例上,也可以放在建構函式的原型(prototype)上。兩者看起來差不多,但在記憶體使用、效能與程式可維護性上會產生顯著差異。
了解 實例方法 與 原型方法 的差別,是掌握 JavaScript 原型繼承(prototype inheritance)與設計高效能物件導向程式的關鍵。
本篇文章將從概念說明、實作範例、常見陷阱到最佳實踐,逐步帶領讀者認識何時該使用實例方法、何時該使用原型方法,並提供實務上常見的應用情境。
核心概念
1. 什麼是實例方法?
實例方法是 在每一次 new 時直接寫入物件本身 的函式。
function Person(name) {
this.name = name; // 實例屬性
this.sayHello = function () { // **實例方法**
console.log(`Hello, I am ${this.name}`);
};
}
- 每建立一個
Person實例,都會產生一個 獨立的函式物件。 - 這樣做的好處是 每個實例可以擁有自己的閉包環境,方便存取建構時傳入的私有變數。
- 缺點是 記憶體開銷較大,大量物件會導致相同的函式被重複建立。
2. 什麼是原型方法?
原型方法是 放在建構函式的 prototype 物件上,所有實例共用同一個函式。
function Person(name) {
this.name = name; // 實例屬性
}
// **原型方法**
Person.prototype.sayHello = function () {
console.log(`Hello, I am ${this.name}`);
};
- 所有
Person實例在查找屬性時,若在自身找不到,就會往prototype鏈上尋找。 - 因此 同一個函式只會被建立一次,記憶體使用更節省,且方法的更新會即時影響所有實例。
3. 為什麼要區分?
| 項目 | 實例方法 | 原型方法 |
|---|---|---|
| 建立時機 | 每次 new 時建立新函式 |
建構函式定義時建立一次 |
| 記憶體消耗 | 較高(每個實例都有副本) | 較低(全體共享) |
| 封閉性 | 可捕獲建構時的私有變數 | 無法直接存取私有變數(除非使用 WeakMap 等技巧) |
| 執行效能 | 呼叫時稍慢(因為函式較多) | 呼叫時較快(同一函式共享) |
| 動態修改 | 必須逐一修改每個實例 | 只要改 prototype 即可影響全部 |
4. 何時選擇實例方法?
- 需要封閉變數:例如每個實例都有獨立的計數器或私有狀態。
- 方法的行為會因實例而異:例如根據建構參數決定不同的實作。
5. 何時選擇原型方法?
- 方法是「共通」的:大多數情況下,所有實例都會執行相同的邏輯。
- 需要節省記憶體:大量建立物件時,原型方法是首選。
- 想要在執行時動態「擴充」或「覆寫」:只要改變
prototype,所有實例立即生效。
程式碼範例
以下示範 4 個常見情境,分別使用實例方法或原型方法,並說明背後的原因。
範例 1️⃣:簡單的 Dog 類別(使用原型方法)
function Dog(name) {
this.name = name; // 實例屬性
}
// **原型方法**:所有 Dog 都會說 "Woof!"
Dog.prototype.bark = function () {
console.log(`${this.name} says: Woof!`);
};
const d1 = new Dog('Lucky');
const d2 = new Dog('Buddy');
d1.bark(); // Lucky says: Woof!
d2.bark(); // Buddy says: Woof!
重點:
bark只被建立一次,兩個實例共用同一個函式,記憶體使用最小。
範例 2️⃣:需要私有變數的 Counter(使用實例方法)
function Counter(initial = 0) {
let count = initial; // **私有變數**,只能被閉包存取
// **實例方法**,每個實例都有自己的閉包
this.increment = function () {
count++;
console.log(`Current count: ${count}`);
};
this.decrement = function () {
count--;
console.log(`Current count: ${count}`);
};
}
const c1 = new Counter(5);
const c2 = new Counter(10);
c1.increment(); // Current count: 6
c2.increment(); // Current count: 11
說明:若把
increment、decrement放到prototype,就無法直接存取count,因此必須使用實例方法。
範例 3️⃣:混合使用 – 共享工具方法、私有實例方法
function Person(name, birthYear) {
this.name = name;
this.birthYear = birthYear;
// 只在建構時需要一次的閉包
const now = new Date().getFullYear();
// **實例方法**:計算年齡時使用私有變數 now
this.getAge = function () {
return now - this.birthYear;
};
}
// **原型方法**:所有 Person 都會有 greet
Person.prototype.greet = function () {
console.log(`Hi, I'm ${this.name}.`);
};
const p1 = new Person('Alice', 1990);
const p2 = new Person('Bob', 1985);
p1.greet(); // Hi, I'm Alice.
p2.greet(); // Hi, I'm Bob.
console.log(p1.getAge()); // 33(假設今年 2023)
技巧:把不需要私有變數的通用行為放到
prototype,只把需要閉包的功能保留為實例方法。
範例 4️⃣:動態改寫原型方法(熱更新)
function Car(model) {
this.model = model;
}
// 原型方法
Car.prototype.start = function () {
console.log(`${this.model} engine started.`);
};
const carA = new Car('Toyota');
carA.start(); // Toyota engine started.
// 之後想要加入 log 功能,只要改 prototype
Car.prototype.start = function () {
console.log(`[LOG] ${new Date().toISOString()}`);
console.log(`${this.model} engine started.`);
};
carA.start(); // [LOG] ... Toyota engine started.
const carB = new Car('Honda');
carB.start(); // 同樣會套用新實作
重點:只要改變
prototype,所有既有與未來的實例都會立即使用新版本,這在插件系統或熱更新時非常有用。
常見陷阱與最佳實踐
1️⃣ 忘記 new,導致 this 指向全域物件
function User(name) {
this.name = name;
}
User.prototype.sayHi = function () {
console.log(`Hi, ${this.name}`);
};
const u = User('Tom'); // ← 忘記 new
// 此時 this === window (或 undefined in strict mode)
解法:在建構函式內部加入保護:
function User(name) {
if (!(this instanceof User)) return new User(name);
this.name = name;
}
2️⃣ 在原型上放置 可變的物件(共享問題)
function Box() {}
Box.prototype.items = []; // 所有 Box 共享同一個陣列
const b1 = new Box();
b1.items.push('apple');
const b2 = new Box();
console.log(b2.items); // ['apple'] ← 不符合預期
最佳實踐:把可變的屬性放在建構子裡:
function Box() {
this.items = []; // 每個實例各自擁有自己的陣列
}
3️⃣ 不必要的實例方法造成記憶體浪費
function Point(x, y) {
this.x = x;
this.y = y;
this.distanceFromOrigin = function () {
return Math.hypot(this.x, this.y);
};
}
改寫:
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.distanceFromOrigin = function () {
return Math.hypot(this.x, this.y);
};
4️⃣ 失誤使用 prototype 替代 class 語法(可讀性)
ES6 class 其實是 語法糖,底層仍然是原型繼承。對於新手,建議先使用 class,再逐步了解背後的 prototype 機制。
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound.`);
}
}
實際應用場景
| 場景 | 建議使用 | 為什麼 |
|---|---|---|
| 大量資料模型(例如 ORM、資料表映射) | 原型方法 | 需要高效能、低記憶體開銷,且方法大多相同。 |
| UI 元件庫(每個元件都有獨立的狀態) | 混合:狀態屬於實例,公共 API 放在原型 | 可共享渲染邏輯,同時保護元件私有狀態。 |
| 函式式編程或 immutable 物件 | 原型方法,配合 Object.freeze |
只需要純粹的行為,避免不必要的閉包。 |
| 需要私有變數的封裝(例如計數器、事件監聽) | 實例方法 + 關閉變數 | 只有實例方法才能持有私有資料。 |
| 插件/擴充系統(需要在執行時改寫行為) | 原型方法(動態改寫) | 只要改 prototype,所有已載入的插件即時生效。 |
總結
- 實例方法適合需要私有閉包或每個實例行為不盡相同的情況,但會產生較大的記憶體開銷。
- 原型方法則是 共享、效能與記憶體最佳化 的首選,特別是大量建立相同類型物件時。
- 在實務開發中,混合使用往往是最靈活的策略:把共通、純粹的行為放在原型,把需要私有狀態的功能留給實例方法。
- 注意避免 共享可變屬性、忘記
new、以及 不必要的實例方法,這些都是新手常踩的坑。 - 最後,掌握了原型與實例方法的差異,就能更自如地設計 可維護、效能佳 的 JavaScript 程式碼,為日後的框架開發或大型專案奠定堅實基礎。
祝你在 JavaScript 的原型世界中寫出更乾淨、更高效的程式! 🚀