# [JavaScript 教學] Callback Function 是什麼?回呼函式和參數傳遞教學 - 用外送比喻一次搞懂回呼函式 ![Callback Function](https://hackmd.io/_uploads/B1MbbRo9le.png) 最近開始接觸 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)