本文 AI 產出,尚未審核

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'); // 正確返回實例

常見陷阱與最佳實踐

  1. 回呼函式丟失 this

    • ✅ 使用 箭頭函式 或 bind(this)。
    • ✅ 若必須使用普通函式,先儲存 const self = this;。
  2. 事件處理器把 this 改成 DOM 元素

    • ✅ 在註冊時 綁定:handler.bind(this)。
    • ✅ 或在類別欄位中直接寫箭頭函式。
  3. 在原型上使用箭頭函式

    • ❌ 會捕獲全域 this,導致指向錯誤。
    • ✅ 只在 建構子內部 或 類別欄位 使用箭頭函式。
  4. 忘記 new 建構子

    • ✅ 使用 工廠函式 或在建構子內檢查 new.target。
    • ✅ 開啟 嚴格模式 ('use strict') 可讓未加 new 時 this 為 undefined,更易發現錯誤。
  5. 混用 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 元素取代,以及 箭頭函式不適用於原型方法。
  • 最佳實踐:
    1. 盡量使用箭頭函式 捕獲外層 this(但不要在原型上)。
    2. 必要時顯式綁定 (bind、call、apply)。
    3. 在類別欄位 中寫方法,可自動綁定 this。
    4. 開啟嚴格模式,讓未綁定的 this 為 undefined,更容易偵測錯誤。
    5. 統一程式風格,減少混用 ES5/ES6 產生的混淆。

只要熟悉四種綁定規則、了解原型鏈的呼叫機制,並在關鍵點使用 bind 或 箭頭函式,就能有效避免 this 的綁定陷阱,寫出 可預測、易維護 的 JavaScript 原型與繼承程式碼。祝你在開發旅程中玩得開心,this 也能成為你的好幫手!