JavaScript Callback (回呼函式)
Callback(回呼函式)是 JavaScript 異步程式設計的基礎概念。簡單來說,callback 就是「把一個函式當作參數傳給另一個函式,等到某件事完成後再執行它」。
什麼是 Callback?
在 JavaScript 中,函式是一級物件 (first-class object),可以像變數一樣被傳遞。當你把一個函式 A 傳給另一個函式 B,讓 B 在適當時機執行 A,這個 A 就是所謂的 callback function。
function greet(name, callback) {
console.log('Hello, ' + name);
callback(); // 執行傳入的 callback 函式
}
function sayGoodbye() {
console.log('Goodbye!');
}
// 把 sayGoodbye 當作 callback 傳入
greet('Mike', sayGoodbye);
// 輸出:
// Hello, Mike
// Goodbye!
同步 Callback
Callback 不一定要用在異步情境。像陣列的 forEach、map、filter 方法,它們接收的函式參數也是 callback,但執行是同步的:
var numbers = [1, 2, 3];
// forEach 的 callback 會同步執行
numbers.forEach(function(num) {
console.log(num * 2);
});
console.log('done');
// 輸出:
// 2
// 4
// 6
// done
異步 Callback
Callback 最常見的用途是處理異步操作。JavaScript 是單執行緒的語言,為了不阻塞程式執行,像網路請求、讀取檔案、計時器這類需要等待的操作,都會採用異步的方式,等操作完成後再透過 callback 通知你。
setTimeout 範例
console.log('開始');
setTimeout(function() {
console.log('2 秒後執行');
}, 2000);
console.log('結束');
// 輸出:
// 開始
// 結束
// 2 秒後執行(2 秒後才出現)
setTimeout 不會阻塞程式,它會先把 callback 「註冊」起來,等 2 秒後再執行。
事件處理範例
DOM 事件處理也是典型的 callback 應用:
document.getElementById('myButton').addEventListener('click', function() {
console.log('按鈕被點擊了!');
});
這裡的匿名函式就是 callback,當使用者點擊按鈕時才會執行。
Callback Hell(回呼地獄)
當你需要依序執行多個異步操作時,callback 會一層套一層,形成所謂的「callback hell」:
// 模擬異步操作
function fetchUser(userId, callback) {
setTimeout(function() {
callback({ id: userId, name: 'Mike' });
}, 1000);
}
function fetchPosts(userId, callback) {
setTimeout(function() {
callback([{ id: 1, title: 'Post 1' }, { id: 2, title: 'Post 2' }]);
}, 1000);
}
function fetchComments(postId, callback) {
setTimeout(function() {
callback([{ id: 1, text: 'Nice!' }]);
}, 1000);
}
// Callback Hell - 層層嵌套,難以閱讀和維護
fetchUser(1, function(user) {
console.log(user);
fetchPosts(user.id, function(posts) {
console.log(posts);
fetchComments(posts[0].id, function(comments) {
console.log(comments);
// 如果還有更多操作,會繼續嵌套下去...
});
});
});
這種結構有幾個問題:
- 程式碼難以閱讀,呈現「金字塔」形狀
- 錯誤處理困難,每一層都要處理錯誤
- 不容易重複使用和測試
錯誤處理
使用 callback 處理異步操作時,常見的模式是把錯誤當作 callback 的第一個參數(稱為 Error-first callback):
function readFile(filename, callback) {
setTimeout(function() {
if (filename === '') {
callback(new Error('檔案名稱不能為空'), null);
} else {
callback(null, '檔案內容...');
}
}, 1000);
}
readFile('test.txt', function(err, data) {
if (err) {
console.error('錯誤:' + err.message);
return;
}
console.log(data);
});
現代替代方案
為了解決 callback hell 的問題,ES6 引入了 Promise,ES2017 又引入了 async/await,讓異步程式碼更容易閱讀和維護。
上面 callback hell 的範例,用 Promise 改寫會變成:
fetchUser(1)
.then(function(user) {
return fetchPosts(user.id);
})
.then(function(posts) {
return fetchComments(posts[0].id);
})
.then(function(comments) {
console.log(comments);
})
.catch(function(err) {
console.error(err);
});
用 async/await 改寫則更直觀:
async function main() {
try {
var user = await fetchUser(1);
var posts = await fetchPosts(user.id);
var comments = await fetchComments(posts[0].id);
console.log(comments);
} catch (err) {
console.error(err);
}
}
雖然有了更現代的語法,但理解 callback 仍然是學習 JavaScript 異步程式設計的基礎。許多 API 和函式庫仍然使用 callback 模式,而且 Promise 和 async/await 底層也是建立在 callback 機制之上。