JavaScript 課程 – 函式(Functions)
主題:回呼函式(Callback Functions)
簡介
在 JavaScript 中,函式是一等公民(first‑class citizens),也就是說函式本身可以被當作變數、參數、甚至回傳值使用。正因如此,回呼函式(callback function)成為非同步程式設計與事件驅動模型的核心概念。
無論是處理使用者點擊、讀取檔案、或是發送 AJAX 請求,幾乎所有需要「等候」的操作都會透過回呼函式告訴程式「工作完成」或「發生錯誤」的時機。掌握回呼函式的寫法與注意事項,對於撰寫可維護、可擴充的 JavaScript 應用程式至關重要。
本篇文章將從 概念、語法、實作範例 逐步說明,並提供常見陷阱與最佳實踐,幫助讀者在實務專案中自信地使用回呼函式。
核心概念
1. 什麼是回呼函式?
回呼函式是一個 被傳入另一個函式作為參數,並在適當的時機被呼叫(invoke)的函式。
簡單來說,A 函式接受 B 函式作為參數,等到 A 完成特定工作後,會「回呼」B 函式。
function doSomething(callback) {
// 執行一些工作…
callback(); // 呼叫傳入的回呼函式
}
重點:回呼函式不一定是同步執行的,最常見的情況是非同步操作(如
setTimeout、fetch)。
2. 同步 vs. 非同步回呼
| 類型 | 何時執行 | 常見使用情境 |
|---|---|---|
| 同步回呼 | 立刻在同一執行緒內執行 | 陣列迭代 (Array.prototype.map) |
| 非同步回呼 | 事件或任務完成後才執行 | setTimeout、XMLHttpRequest、fs.readFile(Node) |
同步回呼範例:Array.map
const numbers = [1, 2, 3, 4];
const doubled = numbers.map(function (n) {
return n * 2; // 這裡的函式即為回呼
});
console.log(doubled); // [2, 4, 6, 8]
非同步回呼範例:setTimeout
console.log('開始');
setTimeout(function () {
console.log('兩秒後執行的回呼');
}, 2000);
console.log('結束');
// 輸出順序:開始 → 結束 → 兩秒後執行的回呼
3. 為什麼要使用回呼?
- 避免阻塞:非同步回呼讓程式在等待 I/O 時仍能繼續執行其他任務。
- 抽象化流程:把「完成後要做什麼」的邏輯交給呼叫者自行決定,提高函式的通用性。
- 事件驅動:在瀏覽器或 Node.js 中,幾乎所有事件(點擊、資料接收)都是以回呼方式處理。
4. 常見的回呼模式
| 模式 | 說明 | 範例 |
|---|---|---|
| 錯誤優先(error‑first) | 第一個參數為 error,若為 null 表示成功,後續參數為結果 |
fs.readFile(path, (err, data) => {...}) |
| 匿名函式 | 直接在呼叫處寫函式,適合一次性使用 | setTimeout(() => {...}, 1000) |
| 具名函式 | 先定義函式,再傳入,方便重複使用或除錯 | array.forEach(handleItem) |
| 高階函式 | 接收多個回呼,形成管線(pipeline) | promise.then(onFulfilled, onRejected)(概念上仍屬於回呼) |
程式碼範例
以下提供 5 個實用範例,從最簡單的同步迭代到 Node.js 的非同步檔案讀取,說明回呼函式的多種寫法與注意點。
範例 1️⃣:陣列的 filter(同步回呼)
// 目標:挑選出陣列中所有偶數
const nums = [1, 2, 3, 4, 5, 6];
const even = nums.filter(function (value) {
// 回呼函式會被 filter 逐一呼叫
return value % 2 === 0;
});
console.log(even); // [2, 4, 6]
說明:
filter內部會把每個元素傳給回呼,回呼回傳true時保留該元素。
範例 2️⃣:setTimeout 的非同步回呼(匿名函式)
console.log('計時開始');
setTimeout(() => {
// 兩秒後執行這段程式碼
console.log('兩秒過去了!');
}, 2000);
console.log('計時結束');
// 輸出順序:計時開始 → 計時結束 → 兩秒過去了!
重點:即使
setTimeout的回呼是匿名函式,仍可在除錯工具中看到() => {}的位置。
範例 3️⃣:錯誤優先回呼(Node.js fs.readFile)
const fs = require('fs');
fs.readFile('data.txt', 'utf8', (err, data) => {
if (err) {
// 錯誤處理
console.error('讀取失敗:', err);
return;
}
// 成功取得檔案內容
console.log('檔案內容:', data);
});
說明:第一個參數
err為null時代表成功,這是 Node.js 常見的回呼慣例。
範例 4️⃣:自訂高階函式接受兩個回呼
function fetchData(url, onSuccess, onError) {
// 假設使用原生 fetch(回傳 Promise)
fetch(url)
.then(response => {
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
})
.then(data => onSuccess(data)) // 成功時呼叫 onSuccess
.catch(err => onError(err)); // 錯誤時呼叫 onError
}
// 使用方式
fetchData(
'https://api.example.com/users',
data => console.log('取得資料:', data),
err => console.error('發生錯誤:', err)
);
技巧:即使
fetch本身回傳 Promise,我們仍可把它包裝成「接受回呼」的 API,讓使用者免除 Promise 的語法。
範例 5️⃣:事件監聽器(DOM)— 回呼與 this 的關係
<button id="myBtn">點我</button>
<script>
const btn = document.getElementById('myBtn');
// 使用普通函式,this 會指向 button 元素
btn.addEventListener('click', function (event) {
console.log('普通函式 this:', this); // <button id="myBtn">
});
// 使用箭頭函式,this 會繼承自外層作用域(此處是 window)
btn.addEventListener('click', (event) => {
console.log('箭頭函式 this:', this); // window
});
</script>
要點:在事件回呼中,普通函式 會把
this绑定到觸發事件的元素;箭頭函式 則不會重新綁定this,常用於不需要this的情況。
常見陷阱與最佳實踐
| 陷阱 | 可能的問題 | 解決方案 / 最佳實踐 |
|---|---|---|
| 回呼地獄(Callback Hell) | 多層巢狀回呼使程式難以閱讀、除錯 | 使用 Promise、async/await 或將回呼抽成具名函式 |
| 忘記錯誤處理 | 異常拋出卻未捕獲,導致程式崩潰 | 採用 錯誤優先 回呼,或在 try/catch 包裹 async 函式 |
this 指向錯誤 |
在回呼內使用 this 時不確定其指向 |
依需求選擇普通函式或箭頭函式,或使用 bind 明確綁定 |
| 多次呼叫回呼 | 同一回呼被意外呼叫多次,產生重複執行 | 在回呼內加入 guard(如 if (called) return;) |
| 未傳入回呼 | 假設傳入的參數一定是函式,卻收到 undefined |
在函式內部 檢查類型:if (typeof callback === 'function') { … } |
建議的編碼風格
- 具名回呼:除非一次性使用,否則給回呼起個有意義的名字,方便除錯與重用。
- 錯誤優先:在 Node.js、瀏覽器 API 中,盡量遵守
(err, result)的簽名。 - 保持單一職責:回呼只負責「完成後要做什麼」,不應同時處理多個步驟。
- 使用
once版回呼:對於只能執行一次的事件(如load),可利用once: true或自行保證只呼叫一次。
實際應用場景
| 場景 | 為何使用回呼 | 範例 |
|---|---|---|
| 表單驗證 | 使用者點擊送出後,先在前端非同步檢查資料,再決定是否送出 | validate(formData, (err, valid) => { if (!err && valid) form.submit(); }) |
| 動畫序列 | 需要在前一段動畫結束後才開始下一段 | fadeOut(elem, () => slideIn(otherElem, () => console.log('完成'))) |
| 第三方 SDK | 大多數外部庫(如 Google Maps、Stripe)以回呼方式返回結果 | stripe.createToken(cardElement, (result) => { … }) |
| 資料串流 | 讀取大型檔案時逐段處理,減少記憶體占用 | stream.on('data', chunk => process(chunk)); |
| 測試 Mock | 在單元測試中模擬非同步回呼,以驗證程式流程 | jest.fn((cb) => cb(null, mockData)); |
總結
回呼函式是 JavaScript 非同步與事件驅動 的基石,從最簡單的陣列迭代到複雜的 I/O 操作,都離不開它。掌握以下要點,即可在實務開發中自如運用:
- 回呼的本質:把「完成後要做什麼」抽象成函式參數。
- 同步 vs. 非同步:根據需求選擇適當的回呼類型。
- 錯誤優先、
this绑定、避免回呼地獄 是常見陷阱的關鍵。 - 最佳實踐:具名回呼、檢查類型、使用 Promise/async‑await 包裝,讓程式更易讀、易維護。
- 實務應用:表單驗證、動畫序列、第三方 SDK、資料串流等,都可以透過回呼實現流暢的使用者體驗。
只要熟練上述概念與技巧,您就能在 JavaScript 專案中有效管理非同步流程,寫出既可靠又易於擴充的程式碼。祝學習順利,開發愉快!