[ Javascript ] Callback function và Higher-order function trong Javascript

     Tiếp tục series về Javascript, hôm nay chúng ta sẽ tìm hiểu về hàm callback trong Javascript.

      Hãy nhớ rằng, trong Javascript, một hàm cũng chính là 1 object, bởi thế hàm sẽ mang nhiều tính chất giống các kiểu dữ liệu thông thường khác như Number, String, Array, … do vậy chúng ta có thể thực hiện những việc như: lưu trữ hàm trong 1 biến, truyền hàm như một tham số vào của hàm khác, tạo ra 1 hàm bên trong hàm, và return hàm như 1 giá trị trả về.

       Chính bởi vì khả năng truyền 1 hàm như là tham số đầu vào của một hàm khác (ta gọi là “hàm-khác” để phân biệt), sau nó được gọi thực thi bên trong “hàm-khác” này, mà ta có khái niệm callback function – mang hàm ý “hàm được gọi thực thi sau”. Ngoài ra, ta gọi “hàm-khác” này là hàm higher-order function.

       Khái niệm truyền hàm như một tham số thực ra không hề mới, nó là một khái niệm của lập-trình-hàm (functional programming). Nếu các bạn đã từng học qua C/C++ thì chắc cũng đã biết về khái niệm “con trỏ hàm”, khái niệm callback function thực ra kế thừa từ đó, chỉ có điều, độ phổ biến của callback function trong Javascript là lớn hơn rất nhiều.

Hàm higher-order và hàm callback là gì?

        Nói một cách đơn giản nhất thì:

  • Hàm higher-order (higher-order function) là hàm có hoạt động dựa trên 1 hàm khác, tức là: nó có thể nhận hàm (function) làm tham số đầu vào, hoặc sẽ return ra 1 hàm khác. Một trong 2 điều kiện đó xảy ra thì được gọi là hàm higher-order.
  • Hàm callback làm hàm được truyền vào “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.

Nghe có vẻ khó hiểu, nhưng hãy xem xét các minh hoạ trực quan hơn sau đây:


function mapArrayString2Length (originalArray, itemFunction)  {
   var newArray = [];
   var newValue;
   var i;
   var len;
   for (i = 0, len = originalArray.length; i < len; i++) {
      newValue = itemFunction(originalArray[ i ]);
      newArray.push(newValue);
   }

   return newArray;
}

function findLength (str)
{
   return str.length
}

   Ta thấy, hàm mapArrayString2Length() có nhận 1 tham số là hàm (tham số itemFunction), như vậy hàm mapArrayString2Length() được gọi là hàm higher-order. Ngoài ra hàm findLength() được truyền vào như là 1 tham số, do đó ta gọi hàm findLength() hoặc tham số itemFunction là hàm callback. Chạy thử đoạn code trên sẽ cho kết quả như sau:


var arr_name = [ 'Ronaldo', 'Messi', 'Suarez' ];
var arr_length = map( arr_name, findLength );     // [7, 5, 6]

Ngoài ra, hãy thử xem thêm hàm higher-order có trả về 1 hàm khác sau đây:

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

Hàm makeMultiplier() cũng được coi là 1 higher-order function.

Ta thường thấy Callback ở chỗ nào trong Javascript?

       Trong Js, nếu xét ở phía client, ngoài những đoạn code xử lí tuần tự thông thường, ta có 2 hoạt động tương đối khác biệt so với những ngôn ngữ server khác, đó là:

  • Lắng nghe event: điển hình như lắng nghe sự kiện click chuột, lắng nghe sự kiện phím enter, …
  • Xử lí bất đồng bộ: Đặc trưng nổi bật của JS là khả năng xử lí bất đồng bộ, có thể kể đến vài hoạt động như: gọi AJAX, đọc file dạng async, …

      Về phần code xử lí lắng nghe event, nếu bạn dùng jQuery thì những callback function sẽ có dạng như thế này:

//Lắng nghe click event, hàm xử lí ta truyền vào chính là 1 callback
$("#btn_1").click(function() {
   alert("Btn 1 Clicked");
});

      Ngoài ra, nếu bạn gọi AJAX, hoặc các xử lí bất đồng bộ tương tự như thế, bạn cũng sẽ sử dụng callback rất nhiều:

function successCallback( jqXHR ) {
    // Do something if success
}

function errorCallback( jqXHR ) {
    // Do something if success
}

$.ajax({
    url: "http://fiddle.jshell.net/favicon.png",
    success: successCallback,
    error: errorCallback
});

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

      Như đã nói ban đầu, hàm trong javascript cũng chính là đối tượng object, chúng ta có thể truyền hàm vào tham số tương tự như cách chúng ta vẫn làm với các kiểu dữ liệu khác vậy.

       Hãy để ý rằng, sự khác biệt quyết định 1 hàm có được thực thi hay không chính là cặp dấu ngoặc “()”, giả sử ta khai báo 1 hàm như sau:

function doSomething( input ) {
    // Do something
}

      Khi đó, nếu ta chỉ đơn thuần gọi tên hàm doSomething mà không có cặp dấu ngoặc, thì đơn thuần là ta vừa gọi tới định nghĩa của hàm, khi ta gọi doSomething() – có cặp dấu ngoặc – thì khi đó hàm mới được thực thi. Vì thế, khi ta truyền hàm đi, ta chỉ đơn thuần sử dụng tên hàm mà không có dấu ngoặc – tức là chỉ truyền đi định nghĩa hàm – có định nghĩa của hàm thông qua tham số rùi, thì hàm higher-order muốn sử dụng callback lúc nào cũng được (kích hoạt bằng cách thêm cặp dấu ngoặc).

Vấn đề gặp phải khi sử dụng callback function

     Do viện dùng hàm trong Javascript tương đối linh hoạt, do vậy ta sẽ thường gặp hai vấn đề chính khi sử dụng callback như sau: đảm bảo context của con trỏ this trong callback, và địa ngục callback (callback hell). Đừng vội hoảng sợ, ta đều sẽ có công cụ để đối phó với việc này.

Đảm bảo context của con trỏ this trong callback

     Có thể bạn đã biết, khi một hàm được kích hoạt, bản thân nó sẽ có một giá trị tham chiếu tới đối tượng vừa gọi nó, giá trị nó nằm ở con trỏ this. Như mọi người đã thấy ở trên, ta có thể truyền hàm callback đi bất kì đâu ta muốn, tức là đối tượng kích hoạt hàm callback này chính là hàm higher-order chứa nó. Tuy nhiên, trong nhiều trường hợp khi thiết kế hàm callback, người dùng mong muốn con trỏ this của hàm callback là 1 đối tượng cụ thể nào khác chứ không phải là hàm higher-order, vậy ta phải xử lí thế nào? (Xem thử minh hoạ sau đây)


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

jQuery('#button_count').click( counter.count_up );  //viết thế này là sai

       Đoạn code trên bắt sự kiện click chuột vào nút button_count để có thể tăng biến đếm của counter, hàm count_up được truyền vào như 1 hàm callback, và vấn đề chính là ở điểm này: khi sự kiện click chuột 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 của hàm count_up đang trỏ tới hàm higher-order gọi nó chứ không phải đối tượng counter.

     Để giải quyết vấn đề này, ta phải chỉ định rõ context của hàm callback ngay từ khi truyền vào. Javascript cung cấp cho ta 3 công cụ là bind(), call()apply(), bạn có thể tìm hiểu kĩ hơn về những công cụ này trong bài viết khác của mình ở LINK NÀY, còn cụ thể lần này thì ta sẽ dùng bind().


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

     Rất nhiều bug xảy ra khi ta không chủ động kiểm soát tốt context của hàm callback khi gọi, vì vậy hãy chú ý tới việc này mỗi khi có ý định sử dụng callback.

Địa ngục callback (callback hell)

      Như ta đã biết, hàm callback được thực thi bên trong 1 hàm khác, nếu ta tiếp tục có hàm callback bên trong một callback khác thì thế nào? Vòng lặp vô tận “callback bên trong callback bên trong callback … ” sẽ có khả năng xảy ra. Thứ quái quỷ này được gọi là callback hell – địa ngục callback, ta sẽ rất hay gặp vấn đề này trong khi xử lí các lệnh bất đồng bộ, kiểu như:


p_client.open(function(err, p_client) {
   p_client.dropDatabase(function(err, done) {
      p_client.createCollection('test_custom_key', function(err, collection) {
         collection.insert({'a':1}, function(err, docs) {
            // ...
            // và nhiều callback nữa
         });
      });
   });
});

     Khi callback hell xuất hiện, logic xử lí của chương trình sẽ trở nên cực kì phức tạp và khó nắm bắt, khi có lỗi xảy ra ta rất khó để debug cũng như giải quyết. Bên cạnh đó, callback hell cũng làm cho tính thẩm mĩ của code giảm đi đáng kể, khó đọc, khó maintain, … Để giải quyết, ta có 2 cách sau đây:

  • Nếu vẫ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 tuy làm thẩm mĩ code trông tốt hơn một chút nhưng thực tế thì code vẫn còn rất tệ và khó đọc.
  • Sử dụng kĩ thuật chạy bất đồng bộ khác: có thể dùng kĩ thuật promise, async và await, … Các kĩ thuật này giải quyết khá triệt để callback hell, mình khuyên mọi người nên tìm hiểu các kĩ thuật này. Chỉ mất 1 chút thời gian tìm hiểu nhưng lợi ích có được là rất rất nhiều.

 Tổng kết

     Xuyên xuốt phần trình bày vừa rồi, mọi người đã có thể hiểu được khái niệm hàm callback trong Javascript là gì, thấy được sự linh hoạt và tính mạnh mẽ của nó, ngoài ra những chỗ thường được ứng dụng kĩ thuật callback cũng đã được trình bày.

      Hàm callback rất linh hoạt và hữu dụng, tuy nhiên nó cũng có những hạn chế nhất định, hi vọng mọi người có thể nắm được những hạn chế của nó và một vài cách khắc phục phổ biến và hiệu quả.

       Chúc các anh em code javascript vui vẻ 🙂

Tham khảo:

  1. What is a simple explanation of higher order functions and callbacks in JavaScript?
  2. Understand JavaScript Callback Functions and Use Them

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s