本文 AI 產出,尚未審核

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


簡介

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

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


核心概念

1. 四大綁定規則概覽

規則 說明 何時產生
預設綁定 (Default Binding) this 會指向全域物件 (windowglobal);在嚴格模式 ('use strict') 下則是 undefined 直接呼叫普通函式 fn()
隱式綁定 (Implicit Binding) 依賴呼叫時的「左側物件」 (obj.method()) 來決定 this obj.method() 形式呼叫
明示綁定 (Explicit Binding) 使用 callapplybind 手動指定 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.mapsetTimeout 等非同步回呼中,使用 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:混用 newcall(避免意外)

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 的情況(如 callapply)時,箭頭函式不會生效。 只在不需要改變 this 的回呼中使用箭頭函式。
多層呼叫的隱式綁定被覆寫 obj.method() 內部再呼叫另一個函式,若未使用 call/applythis 會回到預設。 內層呼叫時使用 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 實例;若在 watchcomputed 裡使用普通函式,需要手動 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 的世界裡玩得開心,寫出更好的程式!