Cơ bảncluster

Callback trong JavaScript: Hiểu và tránh callback hell

8 phút đọc1 lượt xem
#callback javascript#callback là gì#callback hell javascript#hàm callback#xử lý bất đồng bộ javascript

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ì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àmfirst-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.

Về tác giả

Ảnh đại diện tác giả Kenji — họa tiết hình học

Kenji

Kỹ sư phần mềm full-stack (Web), hơn 5 năm kinh nghiệm thực tế

  • Python
  • DB
  • Hạ tầng
  • Đào tạo & cố vấn
  • AI

Làm việc cùng đồng nghiệp người Việt, tôi thấy thiếu tài liệu kỹ thuật rõ ràng bằng tiếng Việt. codeahoc là nơi tôi chia sẻ theo hướng thực tế, dễ áp dụng.

Nguyên tắc nội dung

  • Ưu tiên nguồn gốc và góc nhìn từ thực tế triển khai.
  • Nếu có sai sót, nội dung sẽ được cập nhật và sửa kịp thời.

Khóa học liên quan

The Complete JavaScript Course 2024: From Zero to Expert!

Khóa học JavaScript toàn diện nhất từ cơ bản đến nâng cao.

4.7499.000 ₫
Xem khóa học →

React - The Complete Guide (incl. React Router & Redux)

Làm chủ React.js với các dự án thực tế, hooks, Redux.

4.6499.000 ₫
Xem khóa học →

Node.js, Express, MongoDB & More: The Complete Bootcamp

Backend với Node.js: REST API, authentication, MongoDB.

4.7499.000 ₫
Xem khóa học →
Quảng cáo