JavaScript 原型與繼承:this 綁定陷阱
簡介
在 JavaScript 中,this 是語言最具彈性、同時也是最常讓開發者踩雷的關鍵字。它的值不是在寫程式時決定,而是 在執行時 由呼叫方式決定。尤其在使用原型 (prototype) 與繼承 (inheritance) 時,this 的指向往往會出乎意料,導致程式行為錯誤或難以除錯。
掌握 this 的綁定規則,才能寫出 可預測、可維護 的物件導向程式碼。本篇將從概念說明、實作範例、常見陷阱到最佳實踐,完整解析 this 綁定的「陷阱」與解法,幫助初學者與中階開發者在原型與繼承的情境下,安全地使用 this。
核心概念
1. this 的四種基本綁定
| 綁定方式 | 說明 | 範例 |
|---|---|---|
| 隱式綁定 (Implicit) | 依照物件屬性呼叫的方式決定 this,如 obj.method(),this 為 obj。 |
obj.say(); // this → obj |
| 顯式綁定 (Explicit) | 使用 call、apply、bind 手動指定 this。 |
fn.call(obj); // this → obj |
| 新建綁定 (New) | 使用 new 建構子呼叫時,this 為新建立的實例。 |
new Person(); // this → 新物件 |
| 預設綁定 (Default) | 在非嚴格模式下,未綁定時 this 為全域物件 (window / global);嚴格模式則為 undefined。 |
fn(); // this → window (非 strict) |
注意:箭頭函式 (
=>) 不會 產生自己的this,它會捕獲外層函式的this,因此在原型方法中使用箭頭函式能避免部分綁定問題。
2. 原型方法中的 this
在 ES5 以前,我們常用 建構子 + 原型 的寫法:
function Person(name) {
this.name = name; // new 綁定 → this 為新實例
}
Person.prototype.sayHello = function () {
console.log('Hi, I am ' + this.name);
};
- 呼叫方式:
new Person('Tom').sayHello();
此時sayHello透過 隱式綁定 呼叫,this指向那個 實例。
如果把 sayHello 改寫成箭頭函式:
Person.prototype.sayHello = () => {
console.log('Hi, I am ' + this.name); // this 來自外層 (global)
};
this 會捕獲自外層的 this(在此為全域),失去預期的指向。因此 原型方法 絕不應 使用箭頭函式,除非特別想要繼承外層 this。
3. 繼承鏈與 this
使用原型鏈繼承時,子類別的原型方法仍然遵循上述綁定規則:
function Employee(name, title) {
Person.call(this, name); // 顯式綁定 → this 為 Employee 實例
this.title = title;
}
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;
Employee.prototype.introduce = function () {
// 隱式綁定 → this 為 Employee 實例
console.log(`I'm ${this.name}, a ${this.title}`);
};
若在 introduce 中使用 setTimeout,this 會在回呼函式中被 預設綁定:
Employee.prototype.introduce = function () {
setTimeout(function () {
// 此時 this 為 window (非 strict) → 會找不到 name、title
console.log(`I'm ${this.name}, a ${this.title}`);
}, 100);
};
這是最常見的 this 失效陷阱,下面會詳細說明解法。
4. 程式碼範例:this 失效與修正
範例 1️⃣:回呼函式中的 this(最典型陷阱)
function Counter() {
this.count = 0;
setInterval(function () {
// this 不是 Counter 實例,而是全域物件
this.count++; // NaN 或 undefined
console.log(this.count);
}, 1000);
}
new Counter(); // 會產生錯誤的結果
解法:
- 使用 箭頭函式 捕獲外層
this - 或使用
bind(this) - 或在外層先保存
var self = this;
function Counter() {
this.count = 0;
setInterval(() => {
this.count++; // 正確指向 Counter 實例
console.log(this.count);
}, 1000);
}
new Counter(); // 正常輸出 1,2,3,...
範例 2️⃣:事件處理器中的 this
function Button(id) {
this.el = document.getElementById(id);
this.el.addEventListener('click', this.handleClick);
}
Button.prototype.handleClick = function (e) {
console.log(this === e.currentTarget); // false,this 為 button 元素
console.log(this.el); // undefined
};
new Button('myBtn');
問題:addEventListener 會把 事件目標 (button 元素) 作為 this 傳入,而不是 Button 實例。
解法:
function Button(id) {
this.el = document.getElementById(id);
// 綁定 this 為 Button 實例
this.el.addEventListener('click', this.handleClick.bind(this));
}
Button.prototype.handleClick = function (e) {
console.log(this === e.currentTarget); // true
console.log(this.el.id); // myBtn
};
範例 3️⃣:在類別欄位 (class fields) 中使用箭頭函式
ES6+ 的 class 語法提供 類別欄位,自動將方法綁定 this:
class Logger {
// 類別欄位:自動把 this 綁定到實例
log = (msg) => {
console.log(`[${this.prefix}] ${msg}`);
};
constructor(prefix) {
this.prefix = prefix;
}
}
const logger = new Logger('INFO');
setTimeout(logger.log, 500, 'Hello'); // 正確印出 [INFO] Hello
此寫法等同於在建構子裡 this.log = this.log.bind(this),可避免在回呼中遺失 this。
範例 4️⃣:call / apply 的顯式綁定
function greet(greeting) {
console.log(`${greeting}, ${this.name}`);
}
const user = { name: 'Alice' };
greet.call(user, 'Hi'); // Hi, Alice
greet.apply(user, ['Hey']); // Hey, Alice
在原型繼承情境下,常用 call 把父類別的建構子帶入子類別:
function Animal(name) {
this.name = name;
}
function Dog(name, breed) {
Animal.call(this, name); // 顯式綁定 → this 為 Dog 實例
this.breed = breed;
}
範例 5️⃣:this 與 new 的關係(建構子錯用)
function Car(model) {
this.model = model;
}
const myCar = Car('Tesla'); // 忘記 new
console.log(window.model); // "Tesla" 被掛到全域
解法:加上 new,或在函式內檢查 new.target:
function Car(model) {
if (!new.target) return new Car(model); // 自動補足 new
this.model = model;
}
const myCar = Car('Tesla'); // 正確返回實例
常見陷阱與最佳實踐
回呼函式丟失
this- ✅ 使用 箭頭函式 或
bind(this)。 - ✅ 若必須使用普通函式,先儲存
const self = this;。
- ✅ 使用 箭頭函式 或
事件處理器把
this改成 DOM 元素- ✅ 在註冊時 綁定:
handler.bind(this)。 - ✅ 或在類別欄位中直接寫箭頭函式。
- ✅ 在註冊時 綁定:
在原型上使用箭頭函式
- ❌ 會捕獲全域
this,導致指向錯誤。 - ✅ 只在 建構子內部 或 類別欄位 使用箭頭函式。
- ❌ 會捕獲全域
忘記
new建構子- ✅ 使用 工廠函式 或在建構子內檢查
new.target。 - ✅ 開啟 嚴格模式 (
'use strict') 可讓未加new時this為undefined,更易發現錯誤。
- ✅ 使用 工廠函式 或在建構子內檢查
混用 ES5 原型與 ES6 class
- ✅ 盡量統一風格:若使用
class,就全部使用class。 - ✅ 若必須混用,注意
this的綁定規則仍遵循呼叫方式。
- ✅ 盡量統一風格:若使用
實際應用場景
| 場景 | 常見 this 問題 |
建議解法 |
|---|---|---|
| 前端框架 (Vue / React) 的方法 | 方法被當作回呼傳入時失去 this |
在 Vue 中使用 methods 自動綁定;React class 組件使用 this.handleClick = this.handleClick.bind(this) 或改寫為箭頭函式屬性。 |
| Node.js EventEmitter | 監聽器的 this 為 EventEmitter 本身,若想要外部物件需手動綁定 |
emitter.on('data', this.handleData.bind(this))。 |
| 定時器 / 非同步 API | setTimeout, setInterval, Promise.then 中的 this 會被預設綁定 |
使用箭頭函式或 bind。 |
| 套件開發 (Library) | 公共 API 必須保證 this 正確,以免使用者自行呼叫時出錯 |
在函式入口使用 if (!(this instanceof MyClass)) return new MyClass(...);,或提供工廠方法。 |
| 多層原型繼承 | 子類別呼叫父類別方法時 this 必須指向子類別實例 |
使用 Parent.prototype.method.call(this, ...)。 |
總結
this不是靜態 的變數,而是 依呼叫方式 動態決定的。- 在 原型 與 繼承 的情境下,最常見的問題是 回呼函式失去
this、事件處理器被 DOM 元素取代,以及 箭頭函式不適用於原型方法。 - 最佳實踐:
- 盡量使用箭頭函式 捕獲外層
this(但不要在原型上)。 - 必要時顯式綁定 (
bind、call、apply)。 - 在類別欄位 中寫方法,可自動綁定
this。 - 開啟嚴格模式,讓未綁定的
this為undefined,更容易偵測錯誤。 - 統一程式風格,減少混用 ES5/ES6 產生的混淆。
- 盡量使用箭頭函式 捕獲外層
只要熟悉四種綁定規則、了解原型鏈的呼叫機制,並在關鍵點使用 bind 或 箭頭函式,就能有效避免 this 的綁定陷阱,寫出 可預測、易維護 的 JavaScript 原型與繼承程式碼。祝你在開發旅程中玩得開心,this 也能成為你的好幫手!