回調是什麼:深入解析程式設計中的彈性與效率
你是不是也曾遇過這樣的狀況呢?程式碼寫著寫著,突然遇到一個情境:我希望某個任務執行完畢後,能自動去執行另一段程式碼,但又不想現在就決定那「另一段」具體該做什麼,或是根本無法預知它什麼時候會完成?這時候,你腦袋裡可能就會冒出一個問號:「哇塞,這樣要怎麼辦啦?」別擔心,這可不是你一個人的困惑,許多初入程式設計殿堂的朋友們,剛開始碰到這種需求時,常常會感到有點摸不著頭緒。而這個問題的答案,十之八九都指向一個核心概念,也就是我們今天要深入聊聊的——回調(Callback)。
Table of Contents
回調是什麼?快速了解程式設計的「承諾」機制
簡單來說,回調(Callback)就是一個被作為引數傳遞給另一個函式的函式,並且會在特定的時機點(例如某個事件發生後、非同步操作完成後)被那個接收它的函式呼叫執行。你可以把「回調」想像成這樣:你請一個朋友幫你跑腿買東西,你把錢和購物清單交給他,同時也告訴他:「嘿,買完東西後,記得打電話通知我一聲啊!」這裡的「打電話通知我」就是那個回調,它不是立即執行的,而是在朋友完成任務後,才被動地執行。
在程式設計的世界裡,回調正是實現這種「非同步通知」和「事件驅動」的核心機制。它讓程式碼更加彈性、可擴展,並且是處理網路請求、檔案讀寫、使用者介面互動等非同步操作的基石。如果沒有回調,許多現代應用程式的流暢體驗根本無法實現呢!
為何回調如此重要?深入探索其核心價值
你或許會好奇,既然只是「一個函式呼叫另一個函式」,那跟我們平常寫程式有什麼不一樣?欸,差可大了!回調的核心價值,其實在於它引入了控制反轉(Inversion of Control, IoC)的概念。平常我們寫程式,是我們主動呼叫函式來執行;但有了回調,我們是把一個函式「交出去」,讓別人(另一個函式或系統)在適當的時機幫我們呼叫它。這種「你來決定什麼時候呼叫我」的模式,帶來了幾項關鍵的好處:
非同步處理的基石
這是回調最廣為人知,也最重要的應用場景。想像一下,你的程式碼需要從網路上抓取資料,或是讀取一個大檔案。這些操作通常會耗費一些時間。如果採用同步(Synchronous)的方式,那麼在資料下載或檔案讀取完成之前,你的程式就會一直「卡住」,使用者介面也會凍結,動彈不得。這使用者體驗肯定糟糕透頂,對吧?
透過回調,我們就可以將這些耗時的操作變成非同步(Asynchronous)的。我們發出請求後,立即提供一個回調函式。程式會繼續執行後續的程式碼,而當非同步操作完成後,系統會自動「回頭」來呼叫我們預先定義好的回調函式,通知我們結果。這樣一來,你的程式就能持續保持響應,不會被單一的耗時任務拖垮,使用者介面也能保持流暢,是不是超級棒的?
程式碼的彈性與可擴展性
回調讓你的程式碼模組化程度更高,更具彈性。一個通用的函式,可以透過接受不同的回調函式,來實現不同的行為。舉個例子,你設計了一個處理資料的函式,但對於「資料處理完畢後要怎麼呈現結果」這件事,你不想寫死在函式裡面。這時候,你就可以讓這個處理資料的函式接收一個回調,讓使用者自己決定結果要印在主控台、顯示在網頁上,還是存入資料庫。這樣一來,你的資料處理函式就變得超級通用,而不需要為每個不同的結果處理方式都寫一個新函式,大大提升了程式碼的重複利用率和擴展性。
降低模組間的耦合度
當一個模組或函式需要另一個模組在特定情況下執行某些動作時,直接依賴對方具體的實作細節,就會導致高耦合。但如果使用回調,模組只需要知道「在某個時間點,你需要執行一個我給你的函式」即可,它不需要知道這個函式內部做了什麼。這種設計讓兩個模組之間的關聯性變得鬆散,當一個模組的內部實作發生變化時,另一個模組受到的影響也最小,這對於大型專案的維護和協作來說,簡直是福音啊!
事件驅動程式設計的基礎
現代許多應用程式都是事件驅動的,例如網頁上的按鈕點擊、鍵盤輸入、滑鼠移動等等。這些「事件」的發生是不可預期的。我們不能預設使用者何時會點擊按鈕,所以我們無法在程式碼中「主動」去呼叫一個處理點擊的函式。這時候,回調函式就派上用場了!我們將一個回調函式「註冊」到某個事件上(例如按鈕的點擊事件),當該事件真正發生時,系統就會自動去觸發這個回調函式。這就是典型的「事件監聽器」模式,而回調函式正是這個模式的核心構件。
回調的運作方式:手把手拆解
瞭解了回調的重要性,接著我們就來看看它在程式碼中是怎麼實現的。其實啊,概念並不複雜,歸結起來就兩大步驟:
- 定義回調函式: 首先,你要定義一個「準備在未來被呼叫」的函式。這個函式就是你的回調。
- 將回調函式作為引數傳遞: 接著,你把這個回調函式,當作另一個函式(我們稱之為「高階函式」或「呼叫者函式」)的引數傳遞過去。
- 在高階函式中執行回調: 高階函式在執行到某個特定時機(例如非同步操作完成、或特定事件發生),就會「回頭」來呼叫它所接收到的那個回調函式。
我們拿 JavaScript 這個回調應用最廣泛的語言來舉例說明,這樣會更具體:
function processData(data, callback) {
console.log('開始處理資料...');
// 模擬一個非同步操作,例如網路請求或資料庫查詢
setTimeout(() => {
const processedResult = data.toUpperCase(); // 假設處理邏輯是將資料轉大寫
console.log('資料處理完畢!');
// 當處理完畢後,呼叫傳入的回調函式,並將結果傳遞給它
if (callback && typeof callback === 'function') {
callback(processedResult);
}
}, 2000); // 模擬2秒的延遲
}
// 定義一個回調函式,用於接收處理後的結果
function displayResult(result) {
console.log('在主控台顯示結果:', result);
}
function saveResultToDatabase(result) {
console.log('正在將結果存入資料庫:', result);
// 這裡可以寫入將結果存入資料庫的邏輯
}
// 呼叫 processData 函式,並將 displayResult 作為回調函式傳入
console.log('程式開始執行...');
processData('hello world', displayResult); // 這裡傳入 displayResult
console.log('程式繼續執行其他任務...');
// 當然,你也可以傳入不同的回調函式,實現不同的後續操作
// processData('another piece of data', saveResultToDatabase);
從上面的程式碼片段,你可以清楚看到:processData 函式負責資料處理這個主要任務,它並不關心處理完的結果要怎麼「被用」。而這個「怎麼被用」的責任,就交給了作為引數傳入的 callback 函式。displayResult 和 saveResultToDatabase 就是兩個不同的回調,可以根據需求彈性地替換。
回調的類型:同步與非同步
雖然我們前面一直強調回調在非同步中的應用,但實際上回調函式也分為同步回調和非同步回調喔!
- 同步回調(Synchronous Callback): 這類回調函式在主函式執行過程中,會被立即呼叫執行,並在主函式返回之前完成。它們的執行順序是可預測的。例如,JavaScript 陣列的
forEach、map、filter等方法,它們接收的都是同步回調。const numbers = [1, 2, 3]; numbers.forEach(function(num) { // 這個函式就是同步回調 console.log(num * 2); }); console.log('forEach 迴圈結束。'); // 這會在所有數字處理完畢後才顯示 - 非同步回調(Asynchronous Callback): 這類回調函式不會在主函式呼叫後立即執行,而是在主函式執行完畢、或在某個外部事件(例如計時器到期、網路請求返回)發生後,才被安排在未來的某個時間點執行。
setTimeout、XMLHttpRequest(AJAX) 的事件處理器就是典型的非同步回調。console.log('開始'); setTimeout(function() { // 這個函式就是非同步回調 console.log('延遲了2秒後執行'); }, 2000); console.log('結束'); // 雖然在 setTimeout 後面,但會先執行在上面這個例子中,「結束」會先顯示,然後過了兩秒才顯示「延遲了2秒後執行」。這就是非同步的魅力所在!
回調的實際應用場景
回調函式可說是程式設計中無所不在的概念,尤其在前端開發和 Node.js 後端開發中,你幾乎每天都會和它們打交道:
JavaScript 與 Web 開發
-
事件監聽器(Event Listeners): 這是最常見的應用了。當你點擊一個按鈕、輸入文字、網頁載入完成時,都是透過註冊回調函式來響應這些事件。
document.getElementById('myButton').addEventListener('click', function() { alert('按鈕被點擊了!'); // 這個匿名的函式就是回調 }); -
計時器(Timers):
setTimeout()和setInterval()函式用於在指定延遲後或以固定間隔重複執行某段程式碼。setTimeout(function() { console.log('我等了三秒才出現!'); }, 3000); -
AJAX 請求: 雖然現在我們更常用 Promise 或 Async/Await 來處理 AJAX,但早期的
XMLHttpRequest或底層的fetchAPI 仍然是基於回調的。// 傳統 XMLHttpRequest 的回調範例 const xhr = new XMLHttpRequest(); xhr.open('GET', 'https://api.example.com/data'); xhr.onload = function() { // 這就是一個回調函式 if (xhr.status === 200) { console.log(JSON.parse(xhr.responseText)); } }; xhr.send(); -
陣列方法:
map()、filter()、reduce()、sort()等高階函式都接收回調函式作為引數,用來定義對陣列中每個元素執行的操作。const numbers = [1, 2, 3, 4, 5]; const doubled = numbers.map(num => num * 2); // 箭頭函式也是回調的一種形式 console.log(doubled); // [2, 4, 6, 8, 10]
Node.js 後端開發
Node.js 的核心特性就是其非同步、事件驅動的特性,所以回調在其中扮演著舉足輕重的角色。
-
檔案系統操作(File System Operations): 讀取、寫入檔案等操作都是非同步的,需要提供回調函式來處理結果。
const fs = require('fs'); fs.readFile('example.txt', 'utf8', (err, data) => { // 這個箭頭函式就是回調 if (err) { console.error('讀取檔案失敗:', err); return; } console.log('檔案內容:', data); }); -
網路服務器(HTTP Server): 處理網頁請求也需要回調來響應。
const http = require('http'); http.createServer((req, res) => { // 這個函式就是回調,處理每個進來的請求 res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Hello World!\n'); }).listen(3000); console.log('伺服器執行在 http://localhost:3000/');
回調地獄:美中不足的副作用
儘管回調有這麼多優點,但當你開始將多個非同步操作串聯起來時,就可能會遇到一個令人頭痛的問題,被戲稱為「回調地獄(Callback Hell)」或「金字塔厄運(Pyramid of Doom)」。
什麼是回調地獄?
想像一下,你需要執行一個非同步操作 A,等 A 完成後再執行非同步操作 B,B 完成後再執行 C… 依此類推。如果每個操作都需要一個回調,那麼程式碼就會變成下面這個樣子:
asyncOperationA(function(resultA) {
asyncOperationB(resultA, function(resultB) {
asyncOperationC(resultB, function(resultC) {
asyncOperationD(resultC, function(resultD) {
// ... 你的程式碼會越來越往右縮排,形成一個金字塔
console.log('所有操作都完成了:', resultD);
});
});
});
});
看到沒?一層又一層的巢狀回調,程式碼會不斷向右縮排,可讀性直線下降,除錯起來簡直是惡夢!當出現錯誤時,很難追蹤是哪一個環節出錯。而且錯誤處理也變得異常複雜,你需要為每一層回調都加上錯誤判斷,否則一個環節出錯可能導致整個鏈條斷裂。
回調地獄真的那麼糟糕嗎?
說真的,回調地獄確實很糟糕,它會嚴重影響程式碼的可讀性、可維護性和除錯效率。對於複雜的業務邏輯,如果沒有妥善的處理,很容易讓開發者陷入泥沼。因此,在現代程式設計中,尤其是在處理大量非同步操作時,我們已經有了更優雅的解決方案。
告別回調地獄:現代非同步解決方案
好在,程式設計師們不是傻子,大家很快就意識到回調地獄帶來的痛苦,於是,更先進、更優雅的非同步處理模式應運而生。它們並沒有「取代」回調本身,而是提供了一種更好的方式來「管理」回調,讓程式碼更具可讀性。這些模式,底層依然依賴回調的機制,只是把它們包裝得更美觀、更好用。
Promises(承諾)
Promise 可以說是解決回調地獄的第一道曙光。它代表了一個非同步操作的最終完成(或失敗)及其結果值。Promise 讓你可以將非同步操作的結果像同步操作一樣處理,透過鏈式呼叫 .then() 和 .catch() 來扁平化巢狀回調。
asyncOperationA()
.then(resultA => asyncOperationB(resultA))
.then(resultB => asyncOperationC(resultB))
.then(resultC => asyncOperationD(resultC))
.then(finalResult => {
console.log('所有操作都完成了:', finalResult);
})
.catch(error => {
console.error('發生錯誤:', error); // 統一錯誤處理
});
你看,是不是比巢狀回調乾淨多了?每個 .then() 都返回一個新的 Promise,允許你繼續鏈式呼叫,程式碼像流水線一樣清晰。
Async/Await
這是目前最受歡迎的非同步處理方式,它是基於 Promise 語法糖。async 函式允許你使用 await 關鍵字,讓非同步程式碼寫起來就像同步程式碼一樣,極大地提高了可讀性。await 會「暫停」函式的執行,直到 Promise 被解決(resolve)或拒絕(reject)。
async function performAllOperations() {
try {
const resultA = await asyncOperationA();
const resultB = await asyncOperationB(resultA);
const resultC = await asyncOperationC(resultB);
const resultD = await asyncOperationD(resultC);
console.log('所有操作都完成了:', resultD);
} catch (error) {
console.error('發生錯誤:', error); // 統一錯誤處理
}
}
performAllOperations();
這簡直是把非同步程式碼「拉平」了,可讀性到達了新的高度!async/await 的出現,讓許多開發者大呼過癮,處理非同步邏輯再也不是痛苦的事情了。
Event Emitters(事件發射器)
在 Node.js 中,EventEmitter 模組提供了一種更通用的方式來處理事件。當你的應用程式需要發出多種不同類型的事件,並且有不同的「監聽者」來響應時,EventEmitter 會比單純的回調更有組織性。
我的經驗與感悟
說真的,在我剛開始學習程式設計時,「回調」這個概念真的是讓我開竅的關鍵之一。以前總覺得程式碼就是一步一步往下走,遇到需要等待的,就只能傻傻等。但當我第一次接觸到回調,尤其是非同步回調時,突然覺得眼前一亮:「哇塞,原來程式碼可以這麼靈活!我不用等它,它可以自己回頭來找我!」那種感覺真的超棒的。
雖然現在 Promises 和 Async/Await 讓我們的非同步程式碼寫起來優雅多了,但我還是認為,深入理解回調的本質,以及它為何會帶來「回調地獄」,對於任何一個想精通非同步程式設計的開發者來說,都是不可或缺的。因為不理解回調,你就不會真正明白 Promise 和 Async/Await 到底解決了什麼問題,它們的底層機制是什麼。當你在除錯一些老舊程式碼,或是遇到一些底層庫的 API 時,你還是會看到回調的身影。所以啊,別只顧著用新的糖衣,把裡面的核心給忘了。
我的建議是:先從最簡單的回調開始寫,感受它的彈性。然後當你的非同步邏輯開始變得複雜時,試著用 Promise 來重構,體會它帶來的扁平化。最後,再用 Async/Await 來讓程式碼達到極致的同步感。這樣一步步走過來,你就能真正掌握非同步程式設計的精髓了!程式設計,就是一個不斷學習、不斷進化的過程,不是嗎?
常見相關問題
在了解了回調是什麼之後,你可能還會有以下這些疑問,讓我們一起來深入解答吧!
回調函式和普通函式有什麼不同?
這是一個非常好的問題,也是許多初學者容易混淆的地方。
從表面上看,回調函式和普通函式在語法上可能沒有任何區別,它們都是函式,都有定義、都有可能接收引數、也都有可能返回一個值。然而,它們的根本區別在於「被呼叫的方式」和「執行時機」。
-
普通函式: 你會直接或間接地在程式碼的某處明確地「呼叫」它。例如,
myFunction();就是一個直接呼叫。當你呼叫它時,它會立即執行,並且程式的控制權會移交給這個函式,直到它執行完畢並返回結果,控制權才會回到呼叫它的地方。它的執行流程是線性的、可預測的。function greet() { console.log("哈囉!"); } greet(); // 直接呼叫 console.log("程式繼續。"); // 執行完 greet 後才執行 -
回調函式: 回調函式不會被你直接呼叫。相反,你將它作為引數傳遞給另一個函式。這個「另一個函式」會在未來特定的時間點,自行決定何時、以及是否要呼叫這個回調函式。這就意味著,你放棄了對這個函式「何時執行」的直接控制權。它的執行是「被動」的,可能在當前呼叫堆疊完成之後,或是當某個事件發生之後。
function doSomethingAsync(callback) { console.log("開始執行非同步任務..."); setTimeout(() => { console.log("非同步任務完成!"); callback(); // 在這裡被動地呼叫回調 }, 1000); } doSomethingAsync(() => { console.log("回調函式被執行了!"); }); console.log("主程式碼繼續執行。"); // 可能在回調之前執行
所以,關鍵區別在於:普通函式是你主動呼叫並立即執行,而回調函式是你交給別人,讓別人幫你呼叫,它的執行時機是不確定的(尤其在非同步場景下)。這就是「控制反轉」的精髓。
為什麼會有「回調地獄」?它真的那麼糟糕嗎?
「回調地獄」的產生,主要源於多個非同步操作需要依序執行時,由於每個非同步操作都需要一個回調來處理其結果,並且下一個操作又依賴前一個操作的結果,這導致了回調函式的層層巢狀,程式碼縮排越來越深,形成視覺上的「金字塔」。
它之所以糟糕,原因如下:
- 可讀性極差: 隨著巢狀層級的增加,程式碼會變得難以閱讀和理解。你需要從內到外或從外到內地追蹤邏輯流程,這對人腦來說是很大的負擔。
- 維護困難: 當業務邏輯需要修改或新增一個中間步驟時,你可能需要在多個巢狀層級中插入或修改程式碼,這很容易出錯,並導致意想不到的副作用。
- 錯誤處理複雜: 在回調地獄中,錯誤處理是一個噩夢。你需要為每一層回調都單獨處理錯誤,否則一個深層的錯誤可能會被吞噬,無法向上傳遞,導致整個應用程式狀態不一致或崩潰。如果你不處理,錯誤就可能在某個地方悄無聲息地發生,讓除錯變得異常困難。
- 控制流混亂: 傳統的同步程式碼是自上而下、一步一步執行的。但在回調地獄中,由於非同步的特性,函式調用的順序和實際的執行順序可能大相徑庭,這讓開發者很難推理程式的執行流程。
所以,是的,回調地獄確實非常糟糕,尤其是在處理複雜的非同步流程時。它不僅降低了開發效率,還增加了引入 Bug 的風險。這也是為什麼業界會積極尋求 Promise、Async/Await 等更優雅的非同步解決方案的原因。
Promise 和 Async/Await 取代了回調嗎?
這個問題的答案是:不,它們並沒有「取代」回調,而是提供了一種更優雅、更易於管理的方式來「處理」回調。
想想看,Promise 的 .then() 和 .catch() 方法,它們接收的引數本身就是回調函式。async 函式內部,當你使用 await 關鍵字時,它實際上在等待一個 Promise 解決,而 Promise 內部又是透過回調來通知其狀態變化的。可以說,Promise 和 Async/Await 是基於回調更高層次的抽象和語法糖。
它們的關係更像是:
- 回調: 是非同步操作的底層機制和基本構件。它解決了非同步操作需要「在未來某個時間點通知」的問題。
- Promise: 是對回調的一種結構化封裝。它將非同步操作的成功和失敗狀態標準化,並允許透過鏈式呼叫來扁平化原本巢狀的回調。它解決了回調地獄帶來的可讀性和錯誤處理問題。
- Async/Await: 是 Promise 的語法糖。它讓基於 Promise 的非同步程式碼能夠以一種類似同步的方式書寫,進一步提高了程式碼的可讀性和可維護性。它解決了 Promise 鏈式呼叫有時仍顯冗長的問題。
所以,你可以這樣理解:回調是地基,Promise 是在地基上建造的第一層樓,而 Async/Await 則是在這層樓上設計的精美裝潢。沒有地基,就沒有後面的東西。掌握了回調,你才能更深入地理解 Promise 和 Async/Await 的運作原理。
在哪些程式語言中會用到回調?
回調的概念並非 JavaScript 獨有,它是程式設計中一個非常普遍且強大的模式,廣泛應用於多種程式語言和程式設計範式中。它通常以不同的形式或術語出現,但核心思想都是將一個函式作為引數傳遞給另一個函式,以便在特定時機被呼叫。
以下是一些常見的例子:
- JavaScript: 無疑是回調應用最廣泛的語言。如前所述,事件處理、計時器、非同步 I/O(網路請求、檔案操作)等都大量使用回調。
-
Python: 雖然 Python 更傾向於使用協程(coroutines)和
async/await來處理非同步,但回調也廣泛存在。例如,在多執行緒程式設計中,你可以將一個函式作為threading.Thread的target引數傳遞;或者在某些 GUI 框架(如 Tkinter)中,事件綁定也使用回調。import threading def my_callback(): print("線程任務完成!") thread = threading.Thread(target=my_callback) # my_callback 就是回調 thread.start() -
C/C++: 在 C/C++ 中,回調通常透過函式指標(Function Pointers)來實現。這在作業系統底層、驅動程式、或是需要高度效能且低層次控制的程式碼中非常常見,例如在某些事件迴圈或排序演算法中定義比較邏輯。
// C語言函式指標作為回調範例 void execute_callback(void (*callback_func)()) { printf("執行前...\n"); callback_func(); // 呼叫傳入的函式 printf("執行後。\n"); } void my_action() { printf("這是我在回調中執行的動作。\n"); } int main() { execute_callback(my_action); // my_action 是回調 return 0; } -
Java: 在 Java 8 之前,回調通常透過匿名內部類(Anonymous Inner Classes)或實現特定介面來實現。Java 8 引入了 Lambda 表達式和函式式介面後,使得回調的寫法變得更加簡潔。
// Java Lambda 表達式作為回調範例 interface MyCallback { void onComplete(); } public void doAsyncOperation(MyCallback callback) { System.out.println("Async operation started..."); new Thread(() -> { try { Thread.sleep(1000); // 模擬耗時操作 } catch (InterruptedException e) { e.printStackTrace(); } callback.onComplete(); // 呼叫回調 }).start(); } public static void main(String[] args) { Main app = new Main(); app.doAsyncOperation(() -> { // Lambda 表達式就是回調 System.out.println("Async operation completed!"); }); System.out.println("Main thread continues..."); } - C#: C# 使用委派(Delegates)來實現回調功能。委派是類型安全的函式指標,可以指向一個或多個函式。事件處理是委派最常見的應用。
- Ruby: Ruby 支援程式碼區塊(blocks)、Proc 物件和 Lambda 表達式,這些都可以作為回調函式傳遞。
總之,回調是一個跨語言、跨範式的基礎概念,它的核心思想在於將「行為」作為資料傳遞,實現了控制流的靈活性。
同步回調和非同步回調有何區別?
前面我們已經稍微提到過這兩種回調,但這裡我們可以更詳細地比較它們的關鍵區別,尤其是在執行時機和對程式執行流程的影響方面。
雖然兩者都是「作為引數傳遞的函式」,但它們的「被呼叫方式」和「對主執行緒的影響」截然不同:
-
執行時機:
-
同步回調: 它們在呼叫函式(即接收回調的函式)的執行過程中立即被呼叫執行。回調函式執行完畢後,控制權才會返回到呼叫函式,然後呼叫函式繼續執行其餘的程式碼。這意味著,回調的執行會阻塞主函式的後續程式碼,直到回調完成。它們通常用於陣列迭代(
forEach,map)、排序(sort的比較函式)等情境,其中回調的目的是對資料進行即時處理或變換。function processArraySync(arr, callback) { for (let i = 0; i < arr.length; i++) { callback(arr[i]); // 同步呼叫,會等待 callback 完成 } console.log("陣列處理完成。"); } processArraySync([1, 2, 3], (item) => { console.log("處理中:", item * 10); }); console.log("主程式碼繼續執行。"); // 在所有 item 處理完畢後才執行在上述例子中,”陣列處理完成。” 會在所有 “處理中: …” 顯示完畢後才顯示,接著才是 “主程式碼繼續執行。”。
-
非同步回調: 它們不會在呼叫函式執行時立即被呼叫。相反,呼叫函式會啟動一個非同步操作(例如網路請求、計時器、檔案 I/O),然後立即返回,讓程式碼繼續向下執行。回調函式會在未來某個時間點,當非同步操作完成或特定事件發生時,被事件迴圈或另一個執行緒安排執行。這意味著回調的執行不會阻塞主函式或主執行緒。
function fetchDataAsync(callback) { console.log("開始發起資料請求..."); setTimeout(() => { // 模擬網路請求延遲 console.log("資料請求完成!"); callback("請求到的資料"); // 非同步呼叫,不阻塞主流程 }, 2000); console.log("請求函式已返回。"); // 會在 setTimeout 設置後立即執行 } fetchDataAsync((data) => { console.log("接收到資料:", data); }); console.log("主程式碼繼續執行其他任務。"); // 可能在 '請求到的資料' 之前執行在這個例子中,”開始發起資料請求…” → “請求函式已返回。” → “主程式碼繼續執行其他任務。” 這三句會幾乎同時顯示,然後過兩秒後才顯示 “資料請求完成!” 和 “接收到資料: 請求到的資料”。這就清楚展示了非同步的特性。
-
同步回調: 它們在呼叫函式(即接收回調的函式)的執行過程中立即被呼叫執行。回調函式執行完畢後,控制權才會返回到呼叫函式,然後呼叫函式繼續執行其餘的程式碼。這意味著,回調的執行會阻塞主函式的後續程式碼,直到回調完成。它們通常用於陣列迭代(
-
對主執行緒的影響:
- 同步回調: 會阻塞主執行緒,直到回調執行完畢。
- 非同步回調: 不會阻塞主執行緒,讓程式可以繼續處理其他任務,保持應用程式的響應性。
-
應用場景:
- 同步回調: 邏輯上需要即時處理,例如資料轉換、驗證、函式式程式設計中的高階函式。
- 非同步回調: 處理耗時操作(I/O、網路、計時器),避免介面凍結,實現事件驅動程式設計。
理解這兩者之間的區別,對於掌握非同步程式設計,以及選擇正確的程式設計模式來說,至關重要。

