本文 AI 產出,尚未審核

JavaScript 函式(Functions)— this 綁定規則


簡介

在 JavaScript 中,this 是每一個函式在執行時都會自動取得的特殊參考值。它代表「目前執行上下文的擁有者」,也是物件導向程式設計的核心概念之一。正確掌握 this 的綁定規則,能讓你寫出可讀性高、可維護性好的程式,同時避免在事件處理、非同步呼叫或是 ES6 class 中出現令人困惑的錯誤。

本篇文章將以 淺顯易懂 的方式說明 this 的四大綁定規則(預設、隱式、明示與 new),並提供實務範例、常見陷阱與最佳實踐,幫助從初學者到中級開發者快速提升對 this 的理解與應用能力。


核心概念

1. 四大綁定規則概覽

規則 說明 何時產生
預設綁定 (Default Binding) this 會指向全域物件 (window 或 global);在嚴格模式 ('use strict') 下則是 undefined。 直接呼叫普通函式 fn()
隱式綁定 (Implicit Binding) 依賴呼叫時的「左側物件」 (obj.method()) 來決定 this。 以 obj.method() 形式呼叫
明示綁定 (Explicit Binding) 使用 call、apply、bind 手動指定 this。 fn.call(obj), fn.apply(obj), fn.bind(obj)
建構子綁定 (New Binding) 使用 new 建構子呼叫時,this 會指向新建立的實例物件。 new Constructor()

注意:ES6 arrow function (=>) 會 捕獲外層函式的 this,不會產生自己的 this,因此不受上述四大規則影響。


2. 預設綁定(Default Binding)

function showThis() {
  console.log(this);
}

// 非嚴格模式 → 會印出 window (或 global)
showThis(); // [object Window]

// 嚴格模式 → this 為 undefined
"use strict";
function showStrict() {
  console.log(this);
}
showStrict(); // undefined

重點:在非嚴格模式下,忘記以物件方式呼叫函式,this 會意外指向全域物件,導致變數或屬性被掛在全域,產生難以偵測的 bug。


3. 隱式綁定(Implicit Binding)

const person = {
  name: "Alice",
  greet() {
    console.log(`Hi, I'm ${this.name}`);
  }
};

person.greet(); // Hi, I'm Alice
  • person.greet() 呼叫時,左側的 person 成為 隱式綁定的目標,this 指向 person。
  • 若把方法抽離出來再呼叫,隱式綁定會失效,回到預設綁定:
const greet = person.greet;
greet(); // 在非嚴格模式下印出 window,嚴格模式下印出 undefined

4. 明示綁定(Explicit Binding)

function introduce(age) {
  console.log(`My name is ${this.name}, I'm ${age} years old.`);
}

const bob = { name: "Bob" };

// 使用 call
introduce.call(bob, 30); // My name is Bob, I'm 30 years old.

// 使用 apply(傳入參數陣列)
introduce.apply(bob, [25]); // My name is Bob, I'm 25 years old.

// 使用 bind(產生新函式,延遲執行)
const bobIntro = introduce.bind(bob, 40);
bobIntro(); // My name is Bob, I'm 40 years old.
  • call / apply 立即執行,bind 則回傳一個已綁定 this 的新函式,適合用於事件處理器或作為回呼函式。

5. 建構子綁定(New Binding)

function Car(make, model) {
  this.make = make;
  this.model = model;
}

// 使用 new 建立實例
const myCar = new Car("Toyota", "Corolla");
console.log(myCar.make); // Toyota
console.log(myCar instanceof Car); // true
  • new 會自動在函式內部執行以下三件事:
    1. 建立一個空物件。
    2. 把 this 指向那個新物件。
    3. 若函式沒有回傳其他物件,則回傳 this(即新物件本身)。

6. Arrow Function 與 this 的特殊行為

const obj = {
  id: 1,
  regular() {
    console.log(this.id); // 1
  },
  arrow: () => {
    console.log(this.id); // undefined(因為 arrow 沒有自己的 this)
  }
};

obj.regular(); // 1
obj.arrow();   // undefined
  • Arrow function 會 捕獲外層函式的 this(即 lexical this),在物件字面量中,外層通常是全域或模組範圍,因此 this 不是預期的物件。
  • 常見用法:在 Array.prototype.map、setTimeout 等非同步回呼中,使用 arrow function 可以避免手動 bind(this)。
class Counter {
  constructor() {
    this.count = 0;
  }
  start() {
    setInterval(() => {
      this.count++; // this 仍指向 Counter 實例
      console.log(this.count);
    }, 1000);
  }
}
new Counter().start(); // 每秒遞增

程式碼範例(實用案例)

範例 1:事件處理器中的 this(隱式 vs. 明示)

<button id="btn">點我</button>

<script>
const handler = {
  name: "Button",
  onClick(event) {
    console.log(this.name); // 期望印出 "Button"
    console.log(event.type); // click
  }
};

const btn = document.getElementById('btn');

// 直接傳遞方法 → 隱式綁定會失效,this 變成 button 元素
btn.addEventListener('click', handler.onClick); // this 為 <button>

// 解法 1:使用 bind 明示綁定
btn.addEventListener('click', handler.onClick.bind(handler));

// 解法 2:使用 arrow function 包裝
btn.addEventListener('click', (e) => handler.onClick(e));
</script>

實務建議:在 DOM 事件中,若需要保留自訂物件的 this,請使用 bind 或 arrow 包裝,避免 this 被瀏覽器自動指向事件目標元素。


範例 2:非同步回呼與 this(arrow function)

function DataFetcher(url) {
  this.url = url;
  this.result = null;
}

// 使用 prototype 定義方法
DataFetcher.prototype.load = function () {
  fetch(this.url)
    .then(response => response.json())
    .then(data => {
      // arrow function 捕獲了外層的 this,仍指向 DataFetcher 實例
      this.result = data;
      console.log('Data loaded:', this.result);
    })
    .catch(err => console.error(err));
};

const api = new DataFetcher('https://api.example.com/data');
api.load();

重點:若改用傳統匿名函式 (function(data) { ... }) 而未 bind(this),this 會在 then 中變成 undefined(嚴格模式)或全域物件,導致 this.result 失效。


範例 3:混用 new 與 call(避免意外)

function Person(name) {
  this.name = name;
}

// 正確:使用 new 建構子
const tom = new Person('Tom');
console.log(tom.name); // Tom

// 錯誤示範:直接呼叫 Person,預設綁定會把 name 加到全域
Person('Jerry'); // 在非嚴格模式下 window.name = "Jerry"
console.log(window.name); // Jerry

// 防呆寫法:在函式內檢查是否以 new 呼叫
function SafePerson(name) {
  if (!(this instanceof SafePerson)) {
    return new SafePerson(name);
  }
  this.name = name;
}
const lisa = SafePerson('Lisa'); // 雖然忘記 new,也能正確建立
console.log(lisa.name); // Lisa

最佳實踐:在自訂建構子函式中加入 instanceof 檢查,可提升 API 的容錯性,避免因忘記 new 而污染全域。


範例 4:bind 的延遲執行與部分應用參數

function multiply(a, b) {
  return a * b;
}

// 只綁定第一個參數,留下第二個參數待呼叫時提供
const double = multiply.bind(null, 2);
console.log(double(5)); // 10

// 多層 bind:先綁定 this,再綁定參數
const obj = {
  factor: 3,
  calc: function (x) {
    return x * this.factor;
  }
};

const boundCalc = obj.calc.bind(obj, 4); // this = obj, x = 4
console.log(boundCalc()); // 12

技巧:bind 可用於函式柯里化(currying),在大型程式碼基底中,常用來產生預設參數的專用工具函式。


範例 5:this 在 class 中的自動綁定(ES2022)

class Logger {
  prefix = '[Log]';

  // 使用 class fields 會自動把方法綁定到實例(不需要手動 bind)
  log = (msg) => {
    console.log(`${this.prefix} ${msg}`);
  };
}

const logger = new Logger();
setTimeout(logger.log, 1000, 'Hello'); // 正確印出 [Log] Hello

說明:在 class field 中宣告箭頭函式,可讓方法在被傳遞時自動保留正確的 this,這是 ES2022 之前常見的 this.log = this.log.bind(this) 的簡潔寫法。


常見陷阱與最佳實踐

陷阱 說明 解決方案
忘記 new 建構子函式被直接呼叫,this 變成全域,可能污染 window。 在函式內檢查 instanceof,或使用 class(必須 new)。
事件回呼的 this 被覆寫 addEventListener 中的回呼 this 會指向 DOM 元素。 使用 bind、arrow function 或 event.currentTarget 取得目標。
setTimeout/setInterval 中的 this 同上,回呼會失去原本的 this。 包裝 arrow function 或 bind。
混用箭頭函式與普通函式 在需要動態改變 this 的情況(如 call、apply)時,箭頭函式不會生效。 只在不需要改變 this 的回呼中使用箭頭函式。
多層呼叫的隱式綁定被覆寫 obj.method() 內部再呼叫另一個函式,若未使用 call/apply,this 會回到預設。 內層呼叫時使用 obj.inner.call(this, ...) 或使用箭頭函式捕獲外層 this。

最佳實踐總結

  1. 預設使用 strict mode:可以讓預設綁定的 this 變成 undefined,更容易發現錯誤。
  2. 盡量使用箭頭函式捕獲 this:在回呼、非同步或事件處理器中,避免手動 bind。
  3. 在物件方法中保留隱式綁定:不要把方法抽成獨立變數再呼叫,若需要傳遞,使用 bind。
  4. 建構子函式使用 class:語法更清晰,且會在編譯階段提醒你必須使用 new。
  5. 利用 bind 產生柯里化函式:可以減少重複傳遞相同參數的程式碼。

實際應用場景

  1. React 元件的事件處理

    • 在 class component 中,必須在建構子裡 this.handleClick = this.handleClick.bind(this);,或直接使用屬性箭頭函式 handleClick = () => {...}。
  2. Node.js 中的模組模式

    • 使用 module.exports = function () { ... } 時,若需要存取模組內的屬性,通常會把函式寫成物件方法或使用 class,以免 this 指向 global。
  3. 自訂 Promise / async 函式

    • 在 new Promise((resolve, reject) => { ... }) 內部使用箭頭函式捕獲外層 this,確保可以存取類別實例的屬性。
  4. 資料綁定框架(Vue、Angular)

    • Vue 2 中的 methods 會自動把 this 綁定到 Vue 實例;若在 watch 或 computed 裡使用普通函式,需要手動 bind(this)。
  5. 函式式程式設計(Currying)

    • 透過 bind 產生預設參數的函式,可在大型程式中建立「專用工具」如 const fetchJSON = fetch.bind(null, { headers: { 'Accept': 'application/json' } }),簡化呼叫。

總結

this 是 JavaScript 函式執行時最具挑戰性的概念之一,但只要掌握 四大綁定規則(預設、隱式、明示、new)以及 arrow function 的 lexical this,就能在大多數情境下預測並控制 this 的指向。

  • 預設綁定:全域或 undefined(嚴格模式)。
  • 隱式綁定:左側物件決定 this。
  • 明示綁定:call / apply / bind 手動指定。
  • new 綁定:建構子呼叫產生新實例。

在開發過程中,建議:

  1. 啟用嚴格模式,讓錯誤更顯眼。
  2. 使用箭頭函式 捕獲 this,減少 bind 的使用。
  3. 在需要動態改變 this 時,優先考慮 call / apply。
  4. 對建構子使用 class,確保 new 為唯一入口。

只要遵循上述原則,this 就不再是「恐怖的黑盒子」,而是協助你寫出結構清晰、行為可預測的 JavaScript 程式的有力工具。祝你在 JavaScript 的世界裡玩得開心,寫出更好的程式!