本文 AI 產出,尚未審核

JavaScript 原型與繼承:this 綁定陷阱


簡介

在 JavaScript 中,this 是語言最具彈性、同時也是最常讓開發者踩雷的關鍵字。它的值不是在寫程式時決定,而是 在執行時 由呼叫方式決定。尤其在使用原型 (prototype) 與繼承 (inheritance) 時,this 的指向往往會出乎意料,導致程式行為錯誤或難以除錯。

掌握 this 的綁定規則,才能寫出 可預測可維護 的物件導向程式碼。本篇將從概念說明、實作範例、常見陷阱到最佳實踐,完整解析 this 綁定的「陷阱」與解法,幫助初學者與中階開發者在原型與繼承的情境下,安全地使用 this


核心概念

1. this 的四種基本綁定

綁定方式 說明 範例
隱式綁定 (Implicit) 依照物件屬性呼叫的方式決定 this,如 obj.method()thisobj obj.say(); // this → obj
顯式綁定 (Explicit) 使用 callapplybind 手動指定 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 中使用 setTimeoutthis 會在回呼函式中被 預設綁定

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️⃣:thisnew 的關係(建構子錯用)

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') 可讓未加 newthisundefined,更易發現錯誤。
  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. 必要時顯式綁定 (bindcallapply)。
    3. 在類別欄位 中寫方法,可自動綁定 this
    4. 開啟嚴格模式,讓未綁定的 thisundefined,更容易偵測錯誤。
    5. 統一程式風格,減少混用 ES5/ES6 產生的混淆。

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