本文 AI 產出,尚未審核

JavaScript 課程 – 函式(Functions)

主題:回呼函式(Callback Functions)


簡介

在 JavaScript 中,函式是一等公民(first‑class citizens),也就是說函式本身可以被當作變數、參數、甚至回傳值使用。正因如此,回呼函式(callback function)成為非同步程式設計與事件驅動模型的核心概念。
無論是處理使用者點擊、讀取檔案、或是發送 AJAX 請求,幾乎所有需要「等候」的操作都會透過回呼函式告訴程式「工作完成」或「發生錯誤」的時機。掌握回呼函式的寫法與注意事項,對於撰寫可維護、可擴充的 JavaScript 應用程式至關重要。

本篇文章將從 概念、語法、實作範例 逐步說明,並提供常見陷阱與最佳實踐,幫助讀者在實務專案中自信地使用回呼函式。


核心概念

1. 什麼是回呼函式?

回呼函式是一個 被傳入另一個函式作為參數,並在適當的時機被呼叫(invoke)的函式。
簡單來說,A 函式接受 B 函式作為參數,等到 A 完成特定工作後,會「回呼」B 函式。

function doSomething(callback) {
  // 執行一些工作…
  callback(); // 呼叫傳入的回呼函式
}

重點:回呼函式不一定是同步執行的,最常見的情況是非同步操作(如 setTimeoutfetch)。

2. 同步 vs. 非同步回呼

類型 何時執行 常見使用情境
同步回呼 立刻在同一執行緒內執行 陣列迭代 (Array.prototype.map)
非同步回呼 事件或任務完成後才執行 setTimeoutXMLHttpRequestfs.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. 為什麼要使用回呼?

  1. 避免阻塞:非同步回呼讓程式在等待 I/O 時仍能繼續執行其他任務。
  2. 抽象化流程:把「完成後要做什麼」的邏輯交給呼叫者自行決定,提高函式的通用性。
  3. 事件驅動:在瀏覽器或 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);
});

說明:第一個參數 errnull 時代表成功,這是 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) 多層巢狀回呼使程式難以閱讀、除錯 使用 Promiseasync/await 或將回呼抽成具名函式
忘記錯誤處理 異常拋出卻未捕獲,導致程式崩潰 採用 錯誤優先 回呼,或在 try/catch 包裹 async 函式
this 指向錯誤 在回呼內使用 this 時不確定其指向 依需求選擇普通函式或箭頭函式,或使用 bind 明確綁定
多次呼叫回呼 同一回呼被意外呼叫多次,產生重複執行 在回呼內加入 guard(如 if (called) return;
未傳入回呼 假設傳入的參數一定是函式,卻收到 undefined 在函式內部 檢查類型if (typeof callback === 'function') { … }

建議的編碼風格

  1. 具名回呼:除非一次性使用,否則給回呼起個有意義的名字,方便除錯與重用。
  2. 錯誤優先:在 Node.js、瀏覽器 API 中,盡量遵守 (err, result) 的簽名。
  3. 保持單一職責:回呼只負責「完成後要做什麼」,不應同時處理多個步驟。
  4. 使用 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 操作,都離不開它。掌握以下要點,即可在實務開發中自如運用:

  1. 回呼的本質:把「完成後要做什麼」抽象成函式參數。
  2. 同步 vs. 非同步:根據需求選擇適當的回呼類型。
  3. 錯誤優先this 绑定避免回呼地獄 是常見陷阱的關鍵。
  4. 最佳實踐:具名回呼、檢查類型、使用 Promise/async‑await 包裝,讓程式更易讀、易維護。
  5. 實務應用:表單驗證、動畫序列、第三方 SDK、資料串流等,都可以透過回呼實現流暢的使用者體驗。

只要熟練上述概念與技巧,您就能在 JavaScript 專案中有效管理非同步流程,寫出既可靠易於擴充的程式碼。祝學習順利,開發愉快!