Callback function và Higher-order function trong Javascript

Khái niệm:

  • Higher-order là hàm có hoạt động dựa trên một hàm khác, tức là: nó có thể nhận hàm làm tham số đầu vào, hoặc sẽ trả về một hàm khác. Một trong hai điều kiện đó xảy ra thì được gọi là hàm Higher-order.
  • Callback là hàm được truyền vào một hàm khác như một tham số đầu vào, sau đó sẽ được gọi kích hoạt bên trong hàm khác này.

Ví dụ:

function mapArrayString2Length(array, countLength) {
  var newArray = [];
  var i;
  var length = array.length;
  for (i = 0; i < length; i++) {
    newArray.push(countLength(array[i]));
  }
  return newArray;
}

function countLength(str) {
  return str.length;
}

Như ta thấy được, hàm mapArrayString2Length() có nhận một tham số là hàm (countLength()), như vậy hàm mapArrayString2Length() được gọi là hàm Higher-order. Ngoài ra hàm countLength() được truyền và sử dụng trong hàm mapArrayString2Length(), nên được gọi là hàm Callback.

Chạy thử đoạn code trên ta sẽ có được kết quả như sau:

var arrName = ['Leesin', 'Master Yi', 'Yasuo'];
var arrLength = mapArrayString2Length(arrName, countLength);

// [6, 9, 5]

Ngoài ra, đây là một ví dụ của Higher-order có trả về một hàm khác:

function makeMultiplier(multNum) {
   return function(num) {
     return multNum * num
   };
}
 
// Truyền "hệ số nhân" tuỳ ý để tạo ra các hàm khác nhau
var doubler = makeMultiplier(2);  // Hệ số nhân là 2
var _3x2_ = doubler(3);   // 6
var _4x2_ = doubler(4);  // 8

Callback hoạt động như thế nào?

Sự khác biệt khi ta gọi đến định nghĩa của một hàm và khi ta gọi để thực thi hàm đó là cặp dấu ngoặc (). Giả sử ta có hàm sau

function doSomething() {
 // Do something
}

Khi ta gọi doSomething, tức là ta đang gọi đến định nghĩa của hàm, còn khi ta gọi doSomething() nghĩa là ta đang gọi để thực thi hàm đó. Vì thế, trong Higher-order, khi ta truyền vào đối số là một hàm, ta chỉ truyền vào định nghĩa của hàm (không có dấu ngoặc). Khi có định nghĩa của hàm rồi, thì Higher-order muốn sử dụng Callback lúc nào cũng được (bằng cách gọi hàm có cặp dấu ngoặc).

Các vấn đề gặp phải khi sử dụng Callback

Có hai vấn đề cần chú ý khi ta sử dụng Callback là:

1. Đảm bảo context của con trỏ this trong Callback

Giả sử ta có đoạn code sau:

var counter = {
   count_number: 0,
   count_up: function(){
      this.count_number += 1;
   }
};
 
jQuery('#button_count').click(counter.count_up);

Nhìn lướt qua, chúng ta sẽ thấy được chúng ta đang có một đối tượng counter, trong đó có một trường có tên là count_number và một hàm count_up có chức năng tăng giá trị của count_number của đối tượng counter lên mỗi khi được gọi. Tiếp theo là hành động click nút và gọi đến hàm count_up của đối tượng counter như một Callback. Liệu đoạn code trên có hoạt động? Cùng đọc kĩ lại đoạn code trên nào, đoạn code trên bắt sự kiện click vào button_count, để có thể tăng giá trị của count_number của đối tượng counter, hàm count_up của đối tượng counter được truyền vào như một Callback, và vấn đề xảy ra ở đây là: khi sự kiện click vào button_count được kích hoạt, hàm count_up được gọi nhưng nó không thể tìm thấy biến this.count_number. Lý do là con trỏ this trong hàm count_up đang trỏ tới hàm Higher-order gọi nó tức là hàm click chứ không phải của đối tượng counter. Bởi vì hầu hết thì con trỏ this sẽ mang đúng giá trị của đối tượng gọi hàm, tuy nhiên trong trường hợp này, chúng ta lại cần thay đổi giá trị của con trỏ này sang một đối tượng khác - cụ thể là đối tượng counter. Trong trường hợp này, chúng ta có thể sử dụng hàm bind() do Javascript cung cấp. Hàm bind() này được sử dụng với mục đích trả về cho ta một hàm khác với ngữ cảnh con trỏ this đã được thiết đặt. Nói cách khác, hàm bind() cho phép chúng ta gán giá trị của một đối tượng cụ thể nào đó vào con trỏ this của hàm được kích hoạt. Đoạn code trên sẽ được viết lại như sau:

var counter = {
   count_number: 0,
   count_up: function(){
      this.count_number += 1;
   }
};

jQuery('#button_count').click(counter.count_up.bind(counter));

2. Địa ngục Callback

Như ta đã biết, Callback được thực thi bên trong một hàm, nếu ta tiếp tục gọi một Callback khác bên trong một Callback thì điều gì sẽ xảy ra? Nếu chỉ gọi một, hai lần thì có thể tạm chấp nhận, nhưng nếu gọi n lần (Callback hell) , điều gì sẽ xảy ra? Khi Callback hell xảy ra, nó sẽ làm code trở nên "xấu xí", mất thẩm mĩ và logic trở nên phức tạp, dẫn đến quá trình fix bug, maintain,... rất khó. Để giải quyết ta có hai cách sau:

  • Nếu bạn vẫn muốn sử dụng Callback: ta nên khai báo từng hàm Callback riêng biệt với tên cụ thể, sau đó gọi hàm Callback đầu tiên. Cách này chỉ giảm được một phần nào đó những vấn đề được nhắc ở trên chứ không triệt để hoàn toàn.
  • Sử dụng kĩ thuật chạybất đồng bộ: có thể dùng kĩ thuật promise, async, await,... Các kĩ thuật này giải quyết khá triệt để vấn Callback hell. Để biết thêm về những kĩ thuật này, chúng ta nên tìm hiểu và đi sâu vào chi tiết các kĩ thuật trên.

Tổng kết

  • Trong bài viết trên, mình đã trình bày về khái niệm, cách thức hoạt động cũng như các vấn đề gặp phải khi sử dụng Callback. Hi vọng mọi người có thể biết thêm một phần nào đó về Callback cũng như Higher-order qua bài viết này.
  • Bài viết này là mình tham khảo từ bài viết của vcttai trên trang Những dòng code vui và viết lại theo cách hiểu của mình. Nếu có gì sai xót, mong các bạn bỏ qua.