TypeScript – 函式(Functions)
主題:箭頭函式與 this
簡介
在 JavaScript 以及 TypeScript 中,函式是最基本的抽象單位。隨著 ES6 的推出,箭頭函式(Arrow Function)成為了撰寫函式的新風格,它不僅語法更簡潔,還對 this 的綁定行為做了根本性的改變。
對於 初學者 來說,箭頭函式看起來只是一種寫法的糖衣,但在 實務開發 中,正確掌握它與 this 的互動,往往是避免錯誤、提升程式可讀性的關鍵。本篇文章將從概念、範例、常見陷阱到最佳實踐,完整說明「箭頭函式與 this」在 TypeScript 中的使用方式,幫助你在日常開發中游刃有餘。
核心概念
1. 箭頭函式的語法與特性
| 形式 | 範例 | 說明 |
|---|---|---|
省略 function 關鍵字 |
const add = (a: number, b: number) => a + b; |
直接使用 => 定義函式。 |
| 隱式回傳 | const square = (n: number) => n * n; |
若函式體只有單一表達式,可省略大括號與 return。 |
| 多行程式碼 | typescript\nconst log = (msg: string) => {\n console.log(msg);\n return true;\n};\n |
需要大括號時,必須自行 return。 |
| 參數型別推斷 | const greet = name => Hello, ${name}; |
TypeScript 會根據使用情境推斷型別(若無明確宣告)。 |
重點:箭頭函式本身 不會擁有自己的
this、arguments、super或new.target,它會「捕獲」外層執行環境的this(稱為 lexical this)。
2. this 在傳統函式 vs. 箭頭函式
class Counter {
count = 0;
// 傳統函式
increase() {
setTimeout(function () {
// 這裡的 `this` 為 `window`(或 undefined,在 strict mode)
this.count++; // ❌ 會出錯
}, 100);
}
// 箭頭函式
increaseArrow() {
setTimeout(() => {
// 這裡的 `this` 直接捕獲自 `increaseArrow` 的執行環境
this.count++; // ✅ 正常
}, 100);
}
}
- 傳統函式:在被
setTimeout、setInterval、事件處理器等「脫離」原本呼叫上下文的情況下,this會被重新綁定(預設指向全域物件或undefined)。 - 箭頭函式:
this會固定為宣告時所在的外層物件,不會因為呼叫方式改變而變動,因此常被用來解決this失效的問題。
3. TypeScript 中的 this 型別
TypeScript 允許在函式宣告時明確指定 this 的型別,這對於 普通函式 仍然很有用:
function logThis(this: { name: string }) {
console.log(this.name);
}
const obj = { name: 'Alice', log: logThis };
obj.log(); // Alice
對 箭頭函式,因為 this 是 lexical,不能自行指定型別;若需要在類別內使用,直接依賴類別本身的型別即可。
4. 何時該使用箭頭函式?
| 情境 | 建議使用 | 為什麼 |
|---|---|---|
事件處理器(如 addEventListener) |
✅ 箭頭函式 | 捕獲外層 this,避免 event.target 與 this 混淆。 |
陣列方法的回呼(map、filter、reduce) |
✅ 箭頭函式 | 簡潔、this 不需要變更。 |
需要 new 建立實例 |
❌ 傳統函式 | 箭頭函式沒有 [[Construct]],不可作為建構子。 |
需要 arguments 物件 |
❌ 傳統函式 | 箭頭函式沒有 arguments,若需可使用 rest 參數。 |
5. 範例彙整
範例 1:簡易計數器(使用箭頭函式避免 this 失效)
class Timer {
private seconds = 0;
private id?: number;
start() {
// 使用箭頭函式,this 永遠指向 Timer 實例
this.id = window.setInterval(() => {
this.seconds++;
console.log(`已經過 ${this.seconds} 秒`);
}, 1000);
}
stop() {
if (this.id !== undefined) {
clearInterval(this.id);
}
}
}
const t = new Timer();
t.start();
// 5 秒後呼叫 t.stop();
範例 2:陣列 map 中的 this 捕獲
interface Person {
name: string;
age: number;
}
const people: Person[] = [
{ name: 'Tom', age: 28 },
{ name: 'Jane', age: 22 },
];
class Formatter {
prefix = '>>';
format = (p: Person) => `${this.prefix} ${p.name} (${p.age})`;
// 這裡的 this 直接來自 Formatter 實例
}
const f = new Formatter();
const result = people.map(f.format);
console.log(result); // [ '>> Tom (28)', '>> Jane (22)' ]
範例 3:事件監聽器中的 this(DOM 範例)
const btn = document.createElement('button');
btn.textContent = '點我';
document.body.appendChild(btn);
class Clicker {
count = 0;
// 使用箭頭函式,使 this 永遠指向 Clicker 實例
handleClick = (e: MouseEvent) => {
this.count++;
console.log(`已點擊 ${this.count} 次`);
};
}
const c = new Clicker();
btn.addEventListener('click', c.handleClick);
範例 4:需要 arguments 時的解法
function sum(...nums: number[]) {
// 使用 rest 參數取代舊式的 arguments
return nums.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
備註:若一定要在箭頭函式裡取得呼叫時的參數,請使用 rest 參數,因為箭頭函式沒有自己的
arguments物件。
範例 5:建構子函式不能是箭頭函式
// ❌ 錯誤寫法:箭頭函式無法當作建構子
const Person = (name: string) => {
this.name = name;
};
// const p = new Person('Alice'); // TypeError
// ✅ 正確寫法:使用傳統函式或 class
class PersonClass {
constructor(public name: string) {}
}
const p = new PersonClass('Alice');
console.log(p.name); // Alice
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
this 仍被覆寫 |
在某些情況下,外層 this 本身就不正確(例如把方法直接傳給別的物件) |
使用 .bind(this) 或 const fn = this.method.bind(this),或改寫成箭頭屬性(如 method = () => {})。 |
誤以為箭頭函式可以使用 new |
箭頭函式沒有 [[Construct]]],會拋出錯誤 |
必須使用 class 或 function 宣告建構子。 |
忘記 return(多行程式碼) |
大括號包住的箭頭函式需要手動 return |
確認是否需要回傳值,必要時加上 return,或改寫成單行隱式回傳。 |
使用 arguments |
箭頭函式沒有 arguments 物件 |
改用 rest 參數 (...args)。 |
| 在類別原型上使用箭頭函式 | 會在每次建立實例時重新建立函式,增加記憶體負擔 | 若函式不需要捕獲 this,可使用 原型方法(method(){})以共享同一函式實例。 |
最佳實踐
- 只在需要 lexical
this時使用箭頭函式。若函式不依賴this,傳統方法更具可讀性且效能稍佳。 - 在類別屬性上宣告箭頭函式(如
method = () => {})可確保每個實例都有正確的this,但需留意記憶體使用。 - 使用
strict模式("use strict")避免因this變成全域物件而產生隱蔽錯誤。 - 在 TypeScript 中加入
this型別(只對普通函式有效),提升編譯時的安全性。 - 避免在大型迭代或高頻呼叫的路徑內建立大量箭頭函式,可改為事先定義好方法或使用
bind。
實際應用場景
1. 前端框架的事件綁定(React、Vue)
React 中的類別元件常以箭頭函式寫成事件處理器,避免在
render時每次都重新bind。class MyButton extends React.Component { state = { count: 0 }; // Arrow property ensures correct `this` handleClick = () => { this.setState({ count: this.state.count + 1 }); }; render() { return <button onClick={this.handleClick}>點擊 {this.state.count}</button>; } }Vue 3 的 Composition API 內部大量使用箭頭函式,讓
setup中的this不會被意外改寫。
2. Node.js 中的非同步流程
在 async/await 與 Promise 鏈結中,箭頭函式保持 this 直屬於外層物件,讓服務端程式碼更易讀:
class DB {
private client: any;
constructor(client: any) { this.client = client; }
getUser(id: string) {
return this.client.query('SELECT * FROM users WHERE id = $1', [id])
.then(row => {
// Arrow keeps `this` = DB instance
console.log('查詢完成於', this);
return row;
});
}
}
3. 高階函式(Higher‑Order Functions)
在函式式程式設計中,常會傳遞回呼給其他函式,例如 debounce、throttle。使用箭頭函式可以保證 this 不會因包裝層而失效:
function debounce<T extends (...args: any[]) => any>(fn: T, delay: number) {
let timer: NodeJS.Timeout;
return (...args: Parameters<T>) => {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
總結
- 箭頭函式 以 語法簡潔、lexical
this為核心特性,是現代 TypeScript/JavaScript 開發的常用工具。 - 正確理解
this捕獲機制,能在非同步、事件處理、陣列操作等情境中避免常見的this失效錯誤。 - 仍有 限制(無
new、無arguments、每個實例會產生新函式),使用前應評估效能與記憶體需求。 - 在 實務專案(前端框架、Node.js 服務、函式式程式設計)中,適當選擇箭頭函式或傳統函式,搭配 TypeScript 的型別檢查,可提升程式碼的安全性與可維護性。
掌握了箭頭函式與 this 的互動原理,你就能在撰寫 TypeScript 程式時更加得心應手,寫出 乾淨、可靠且易於維護 的程式碼。祝開發順利!