Callback trong JavaScript: Hiểu và tránh callback hell
Khi học JavaScript, bạn sẽ sớm gặp khái niệm callback. Đây là một trong những khái niệm cơ bản nhất nhưng cũng dễ gây nhầm lẫn cho người mới bắt đầu. Hiểu rõ callback sẽ giúp bạn nắm vững cách JavaScript xử lý các tác vụ bất đồng bộ — nền tảng để làm việc với API, sự kiện người dùng và nhiều tính năng quan trọng khác.
Trong bài viết này, chúng ta sẽ tìm hiểu callback là gì, cách sử dụng, và cách tránh "callback hell" — cái bẫy mà nhiều lập trình viên mới mắc phải. Trước khi đọc bài này, bạn nên tìm hiểu JavaScript là gì và hàm trong JavaScript để có nền tảng vững chắc hơn.
Callback trong JavaScript là gì?
Callback là một hàm được truyền vào hàm khác dưới dạng đối số (tham số), và được gọi lại ở một thời điểm nào đó bên trong hàm đó.
Hãy tưởng tượng bạn gọi điện cho bạn bè và nói: "Khi nào xong việc thì gọi lại cho mình nhé!" Đó chính là ý tưởng của callback — bạn đưa ra một "hành động cần làm sau" và để hàm khác quyết định khi nào thực hiện hành động đó.
Trong JavaScript, hàm là first-class citizen (công dân hạng nhất), nghĩa là hàm có thể được lưu vào biến, truyền làm đối số, hoặc trả về từ hàm khác — giống như bất kỳ giá trị nào khác (số, chuỗi...).
Cú pháp cơ bản của Callback
Hãy xem ví dụ đơn giản nhất về callback:
function greet(name, callback) {
console.log("Xin chào, " + name); // In lời chào
callback(); // Gọi hàm callback
}
function sayBye() {
console.log("Tạm biệt!");
}
greet("An", sayBye); // Truyền sayBye làm callback
// Kết quả:
// Xin chào, An
// Tạm biệt!
Lưu ý quan trọng: khi truyền sayBye làm callback,
bạn không được thêm dấu () sau tên hàm:
greet("An", sayBye); // ✓ Đúng: truyền tham chiếu hàm
greet("An", sayBye()); // ✗ Sai: gọi sayBye ngay lập tức, truyền kết quả (undefined)
Bạn cũng có thể truyền hàm vô danh (anonymous function) trực tiếp làm callback:
greet("Bình", function() {
console.log("Hẹn gặp lại!");
});
// Kết quả:
// Xin chào, Bình
// Hẹn gặp lại!
Hoặc dùng arrow function để viết ngắn gọn hơn:
greet("Chi", () => {
console.log("Chúc một ngày tốt lành!");
});
Callback bất đồng bộ (Asynchronous Callback)
Callback thực sự tỏa sáng trong lập trình bất đồng bộ. JavaScript chạy trên một luồng duy nhất (single-threaded), có nghĩa là nó chỉ xử lý một việc tại một thời điểm. Nếu phải chờ một tác vụ chậm (gọi API, đọc file...) mà không dùng bất đồng bộ, toàn bộ trang web sẽ bị "đơ".
Hãy xem ví dụ với setTimeout:
console.log("Bắt đầu");
setTimeout(function() {
console.log("3 giây sau...");
}, 3000); // 3000ms = 3 giây
console.log("Kết thúc");
// Thứ tự thực tế:
// Bắt đầu ← in ngay lập tức
// Kết thúc ← in ngay lập tức
// 3 giây sau... ← in sau 3 giây
Bạn có thể ngạc nhiên khi thấy "Kết thúc" xuất hiện trước "3 giây sau...".
Lý do: JavaScript đăng ký setTimeout rồi tiếp tục chạy
dòng tiếp theo ngay lập tức, không chờ. Sau 3 giây, hàm callback mới được gọi.
document.getElementById("myButton").addEventListener("click", function() {
console.log("Nút đã được nhấn!");
});
// Callback chỉ chạy khi người dùng nhấn nút
Callback Hell là gì và tại sao cần tránh?
Vấn đề xảy ra khi bạn cần thực hiện nhiều tác vụ bất đồng bộ liên tiếp, mỗi tác vụ phụ thuộc vào kết quả của tác vụ trước. Khi đó, callback lồng trong callback tạo ra cấu trúc lõm sâu — gọi là Callback Hell (hay "Pyramid of Doom" — Kim tự tháp Diệt vong):
getData(function(a) {
getMoreData(a, function(b) {
getEvenMoreData(b, function(c) {
console.log(c); // Đọc rất khó!
});
});
});
// Đăng nhập → Lấy hồ sơ → Lấy lịch sử đơn hàng → Lấy chi tiết sản phẩm
login(username, password, function(user) {
getUserProfile(user.id, function(profile) {
getOrderHistory(profile.id, function(orders) {
getProductDetail(orders[0].productId, function(product) {
console.log("Tên sản phẩm:", product.name);
// Và còn có thể lồng tiếp...
});
});
});
});
- Khó đọc: Phải theo dõi nhiều cấp lồng nhau để hiểu luồng xử lý
- Khó debug: Khi có lỗi, rất khó xác định lỗi xảy ra ở đâu
- Xử lý lỗi phức tạp: Mỗi callback cần xử lý lỗi riêng
- Khó bảo trì: Sửa đổi hoặc thêm tính năng mới trở nên rủi ro
Cách thoát khỏi Callback Hell
function handleProduct(product) {
console.log("Tên sản phẩm:", product.name);
}
function handleOrders(orders) {
getProductDetail(orders[0].productId, handleProduct);
}
function handleProfile(profile) {
getOrderHistory(profile.id, handleOrders);
}
function handleUser(user) {
getUserProfile(user.id, handleProfile);
}
login(username, password, handleUser);
Promise trong JavaScript là giải pháp được giới thiệu từ ES6 (2015):
login(username, password)
.then(user => getUserProfile(user.id))
.then(profile => getOrderHistory(profile.id))
.then(orders => getProductDetail(orders[0].productId))
.then(product => console.log("Tên sản phẩm:", product.name))
.catch(error => console.error("Lỗi:", error));
async function getProductInfo() {
try {
const user = await login(username, password);
const profile = await getUserProfile(user.id);
const orders = await getOrderHistory(profile.id);
const product = await getProductDetail(orders[0].productId);
console.log("Tên sản phẩm:", product.name);
} catch (error) {
console.error("Lỗi:", error);
}
}
Khi nào nên dùng Callback?
const numbers = [1, 2, 3, 4, 5];
numbers.forEach(function(num) {
console.log(num * 2);
});
// Kết quả: 2, 4, 6, 8, 10
const chanSo = numbers.filter(num => num % 2 === 0);
console.log(chanSo); // [2, 4]
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
- Dùng callback cho:
forEach,map,filter, event listener, tác vụ đơn giản 1 cấp - Dùng Promise / async-await cho: nhiều tác vụ bất đồng bộ liên tiếp, cần xử lý lỗi rõ ràng
Tổng kết
- Callback là gì: hàm được truyền vào hàm khác và được gọi lại sau đó
- Callback đồng bộ: thực thi ngay sau khi hàm chứa nó hoàn thành
- Callback bất đồng bộ: thực thi khi sự kiện xảy ra hoặc sau một khoảng thời gian
- Callback Hell: vấn đề lồng callback quá sâu gây khó đọc và khó bảo trì
- Giải pháp: đặt tên hàm, dùng Promise hoặc async/await
Callback là nền tảng của JavaScript bất đồng bộ. Hãy thực hành các ví dụ trong bài bằng cách mở Console của trình duyệt (nhấn F12) và gõ thử từng đoạn mã. Thực hành là cách nhanh nhất để ghi nhớ!
Bước tiếp theo, hãy học về Promise trong JavaScript — giải pháp hiện đại để xử lý bất đồng bộ mà không rơi vào callback hell.