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會自動在函式內部執行以下三件事:- 建立一個空物件。
- 把
this指向那個新物件。 - 若函式沒有回傳其他物件,則回傳
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。 |
最佳實踐總結
- 預設使用
strict mode:可以讓預設綁定的this變成undefined,更容易發現錯誤。 - 盡量使用箭頭函式捕獲
this:在回呼、非同步或事件處理器中,避免手動bind。 - 在物件方法中保留隱式綁定:不要把方法抽成獨立變數再呼叫,若需要傳遞,使用
bind。 - 建構子函式使用
class:語法更清晰,且會在編譯階段提醒你必須使用new。 - 利用
bind產生柯里化函式:可以減少重複傳遞相同參數的程式碼。
實際應用場景
React 元件的事件處理
- 在 class component 中,必須在建構子裡
this.handleClick = this.handleClick.bind(this);,或直接使用屬性箭頭函式handleClick = () => {...}。
- 在 class component 中,必須在建構子裡
Node.js 中的模組模式
- 使用
module.exports = function () { ... }時,若需要存取模組內的屬性,通常會把函式寫成物件方法或使用class,以免this指向global。
- 使用
自訂 Promise / async 函式
- 在
new Promise((resolve, reject) => { ... })內部使用箭頭函式捕獲外層this,確保可以存取類別實例的屬性。
- 在
資料綁定框架(Vue、Angular)
- Vue 2 中的
methods會自動把this綁定到 Vue 實例;若在watch或computed裡使用普通函式,需要手動bind(this)。
- Vue 2 中的
函式式程式設計(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 綁定:建構子呼叫產生新實例。
在開發過程中,建議:
- 啟用嚴格模式,讓錯誤更顯眼。
- 使用箭頭函式 捕獲
this,減少bind的使用。 - 在需要動態改變
this時,優先考慮call/apply。 - 對建構子使用
class,確保new為唯一入口。
只要遵循上述原則,this 就不再是「恐怖的黑盒子」,而是協助你寫出結構清晰、行為可預測的 JavaScript 程式的有力工具。祝你在 JavaScript 的世界裡玩得開心,寫出更好的程式!