Callback function và Higher-order function trong JavaScript
Trong bài này Bizfly Cloud sẽ trình bày về một trong những khái niệm cốt lõi và mạnh mẽ làm nên Javascript. Trong JavaScript, một hàm cũng chính là một 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ư một giá trị trả về.
Chính bởi vì khả năng truyền một 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 một 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 một ví dụ về 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à một higher-order function.
Callback thường được dùng ở 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
});
Cơ chế hoạt động của Callback
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 một 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 một 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).
Một vài 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à một đố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 chương trình sẽ chạy không đúng
Đ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ư một 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() và apply(). Trong lần này, 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 một 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 (hay địa ngục callback). Bạn 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ó quản lý, … Dưới đây là hai giải pháp cho callback hell:
• 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.
Kết luận
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ả.
Theo kipalog.com
Bizfly Cloud là nhà cung cấp dịch vụ điện toán đám mây với chi phí thấp, được vận hành bởi VCCorp.
Bizfly Cloud là một trong 4 doanh nghiệp nòng cốt trong "Chiến dịch thúc đẩy chuyển đổi số bằng công nghệ điện toán đám mây Việt Nam" của Bộ TT&TT; đáp ứng đầy đủ toàn bộ tiêu chí, chỉ tiêu kỹ thuật của nền tảng điện toán đám mây phục vụ Chính phủ điện tử/chính quyền điện tử.
Độc giả quan tâm đến các giải pháp của Bizfly Cloud có thể truy cập tại đây.
DÙNG THỬ MIỄN PHÍ và NHẬN ƯU ĐÃI 3 THÁNG tại: Manage.bizflycloud