# [JavaScript 教學] Callback Function 是什麼?回呼函式和參數傳遞教學 - 用外送比喻一次搞懂回呼函式

最近開始接觸 React,發現跟 Vue 比起來更常用到 callback function 的概念,所以決定要來好好複習一下,到底什麼是 callback function!
## 什麼是 Callback Function?
用簡單白話一點的方式來說就是「傳進去、等前面的事情完成後再執行」的函式。
* 把一個函式當作參數「傳進去」另一個函式中
* 當某件事完成時,再「回過頭來」執行這個函式
* 所以叫做 callback (回呼),事情做好了,「回頭來呼叫你」
> 想像我們打電話叫外送的時候:
> 在我們下訂單時,都會留一支聯絡電話📞(callback function)。
> 食物送到之後,外送員就會打那支電話通知我們(執行 callback function)。
Callback 並不是什麼神奇的語法或是艱深難懂的函式,只是 JS 把「函式可以當作變數傳來傳去」的這個能力,應用出來的一個機制。
## 👀 實際應用
以下就是一個 callback function 的簡單範例:
```javascript!
function greet(name, callback) {
console.log(`Hi ${name}`);
callback(); // 呼叫傳進來的函式
}
function sayBye() {
console.log("Bye!");
}
greet("Pear", sayBye);
```
但是單看以上的例子,可能還是看不出來 callback function 的好用之處,其實我們平常在寫 JS 處理非同步操作的時候就很常用到,像是 `setTimeout`、`addEventListener`,以及 API 請求。
### setTimeout
```javascript!
setTimeout(sayHi, 1000); // 這裡的 sayHi 就是 callback function
function sayHi() {
console.log('Hi Pear')
}
```
- 當 JavaScript 執行到 `setTimeout(sayHi, 1000);` 這行時,它會告訴瀏覽器或 Node.js 環境:「嘿,請你在 1 秒鐘之後,幫我執行一下 sayHi 這個函式。」
- JavaScript 不會等待這 1 秒鐘。它會繼續執行 setTimeout 後面的其他程式碼。
- 1 秒鐘過後,環境會自動去「回呼」之前傳給它的 `sayHi` 函式,這時就會在控制台看到 `Hi Pear`。
### API 請求
在 ES6 之前,也就是還沒有 Promise 跟 async/await 的時代,我們呼叫 API 通常不會立馬拿到資料,所以我們可以使用 callback function,等資料來了之後再執行我們想做的事。
```javascript!
function getUser(callback) {
fetch("https://jsonplaceholder.typicode.com/users/1")
.then(response => response.json())
.then(data => {
// 呼叫 callback,並把資料傳出去
callback(null, data);
})
.catch(error => {
// 若有錯誤,也傳給 callback 處理
callback(error, null);
});
}
getUser((err, userData) => {
if (err) {
console.error("發生錯誤:", err);
} else {
console.log("成功拿到使用者資料:", userData);
}
});
```
上面的例子只是單純的想拿使用者資料,如果又新增了更多的需求:
`取得使用者資料 -> 根據使用者 ID 取得貼文 -> 根據貼文 ID 取得留言`
```javascript!
getUser((user) => {
getPosts(user.id, (posts) => {
getComments(posts[0].id, (comments) => {
console.log("Got everything!", comments);
});
});
});
```
此時的程式碼變得一層又一層,且不停地往下接力,看了非常痛苦😵💫,這就是所謂的 callback hell。
當有很多非同步操作必須「一個接一個執行」,而每一個都使用 callback function 嵌套在前一個裡面,導致程式變得很難閱讀跟維護,像掉進「地獄」一樣。
幸好我們現在有了新的做法!
**使用 Promise**
```javascript!
function getUser() {
return fetch("https://jsonplaceholder.typicode.com/users/1")
.then(res => res.json());
}
function getPosts(userId) {
return fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`)
.then(res => res.json());
}
function getComments(postId) {
return fetch(`https://jsonplaceholder.typicode.com/comments?postId=${postId}`)
.then(res => res.json());
}
getUser()
.then(user => {
console.log("使用者資料:", user);
return getPosts(user.id);
})
.then(posts => {
console.log("貼文資料:", posts);
return getComments(posts[0].id);
})
.then(comments => {
console.log("留言資料:", comments);
})
.catch(error => {
console.error("發生錯誤:", error);
});
```
**使用 async/await**
```javascript!
function getUser() {
return fetch("https://jsonplaceholder.typicode.com/users/1")
.then(res => res.json());
}
function getPosts(userId) {
return fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`)
.then(res => res.json());
}
function getComments(postId) {
return fetch(`https://jsonplaceholder.typicode.com/comments?postId=${postId}`)
.then(res => res.json());
}
async function fetchAll() {
try {
const user = await getUser();
console.log("使用者資料:", user);
const posts = await getPosts(user.id);
console.log("貼文資料:", posts);
const comments = await getComments(posts[0].id);
console.log("留言資料:", comments);
} catch (err) {
console.error("發生錯誤:", err);
}
}
fetchAll();
```
不得不說 async/await 的出現真是一大福音,處理 API 的非同步變得更加乾淨利落。
> 以前靠 callback,但會遇到 callback hell,ES6 開始有 Promise 改善,後來 ES8 用 async/await 更直覺。
## Callback Function 傳參數
Callback function 本身也是可以傳參數的,只要用**匿名函式**包起來就可以。
```javascript!
function greet(callback) {
console.log("Hi");
callback(); // 呼叫傳進來的函式
}
greet(() => {
sayBye('Pear');
});
function sayBye(name) {
console.log(`Bye ${name}!`);
}
```
---
## React 上的常見應用
### 事件處理(Event Handling)
React 重視「事件處理」,而事件處理 = callback
```jsx!
function MyButton() {
const handleClick = () => {
console.log('Clicked!');
};
return (
<button onClick={handleClick}>Click me</button>
);
}
```
`onClick` 接的就是一個 callback function,也就是當使用者點擊按鈕時才執行。
而且除了 `onClick`,還會碰到一堆像是:
* `onChange`
* `onSubmit`
* `onMouseEnter`
...只要是事件,就幾乎都會傳 callback。
### 父元件傳遞 callback 給子元件
傳函式當 props 下去,子組件再呼叫
```jsx!
// ParentComponent.jsx
import React from "react";
import ChildComponent from "./ChildComponent";
function ParentComponent() {
const handleClickBtn = () => {
console.log("點擊子組件的按鈕");
};
return <ChildComponent onClickBtn={handleClickBtn} />;
}
export default ParentComponent;
```
```jsx!
// ChildComponent.jsx
function ChildComponent({ onClickBtn }) {
return <button onClick={onClickBtn}>Click me</button>;
}
export default ChildComponent;
```
把 `handleClickBtn` 傳進子組件,等子組件想「通知」父組件的時候,就會呼叫這個 function,這其實也是 callback。
### Hooks 中的 Callback Function (e.g., useEffect, useCallback, useMemo)
- `useEffect`: `useEffect` 的第一個參數就是一個 callback function,它會在元件渲染後執行。
```jsx!
import React, { useEffect, useState } from 'react';
function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('元件渲染或 count 改變');
// 執行任何操作
}, [count]); // 當 count 改變時執行
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Add</button>
</div>
);
}
```
這裡 `() => { ... }` 就是 `useEffect` 的 callback function。
---
希望這篇文章讓大家對 callback function 有了更清楚的認識。
這其實不是什麼神祕的黑魔法,而是 JavaScript 把函式可以當參數傳來傳去這個特性,活用到日常開發裡。
尤其像 React 這種框架,callback 幾乎是日常寫法的一部分。不管是事件處理、資料傳遞還是自訂 hook,都會遇到 callback。
學會 callback,也是邁向更進階 JavaScript / 前端開發的入門門檻。
Leetcode 題目也常常可以看到 callback function 的身影,可參考:
* [2635. Apply Transform Over Each Element in Array #Basic Array Transforms](https://medium.com/@liz665319/leetcode-30-days-of-lc-javascript-challenge-be92d98e0d64)
* [2634. Filter Elements from Array #Basic Array Transforms](https://medium.com/@liz665319/leetcode-30-days-of-lc-javascript-challenge-abf81249b44e)
此文同步發佈於 Medium:
[[JavaScript 教學] Callback Function 是什麼?回呼函式和參數傳遞教學](https://medium.com/@liz665319/javascript-callback-function-347084a19cd7)