Bài này viết về cái gì?

Javascript, ES5, ES6, Functional Programming, Callback, Callback-Hell, Async, Sync, Blocking, Non-Blocking, Anonymous Function, Arrow Function, Higher Order Function, Promises, Async/Await, Observables, Observer, Reactive-Extension RxJS... Bạn quan tâm và muốn hiểu rõ về các khái niệm trên đây, muốn chạy thử để xem cách nó hoạt động ra sao? Bạn hãy đọc bài này.

Bài viết này sẽ tổng hợp và giải thích một số khái niệm từ dễ hiểu đến khó hiểu của Javascript, hi vọng sẽ giúp các bạn hiểu rõ hơn và áp dụng được vào dự án code của các bạn. Vì là dành cho những người thậm chí còn chưa biết gì về Javascript nên có nhiều chỗ tôi giải thích khá "tỉ mẩn", hướng dẫn từng bước một cách quá đáng, nên các bạn biết rồi thì cứ bỏ qua nhé.

Giới thiệu

Trước tiên, phải nhắc lại định nghĩa một chút, Javascript là một ngôn ngữ lập trình hàm. Lập trình hàm hay Functional Programming có cách viết code khác so với các ngôn ngữ lập trình Hướng đối tượng (Object Oriented Programming - OOP) như C# hoặc Java, C++. Lập trình hàm tức là mọi thứ đều là hàm. Khi viết code bằng Javascript thường thì chúng ta chỉ viết hàm, và sử dụng các biến (các object) để chứa dữ liệu. Chỉ cần 2 khái niệm HÀMBIẾN thôi là chúng ta đã bắt đầu code được. Khá dễ dàng phải không.

Javascript cũng chạy được ngay trên trình duyệt, vì thế bạn không cần cài gì để bắt đầu học js, chỉ có trình duyệt web là có thể nghịch Javascript. Tôi thì hay dùng Chrome nhưng các bạn có thể dùng Firefox hoặc Safari, Opera, Edge...đều được.

Asynchronous và Synchronous, Blocking và Non-Blocking

Tôi sẽ viết thử một ví dụ code bằng javascript như sau:

function ShowNum1(){
	console.log('1');
}
function ShowNum2(){
	console.log('2');
}
function ShowNum3(){
	console.log('3');
}
ShowNum1();  //dòng lệnh 1 cần thực thi
ShowNum2();  //dòng lệnh 2 cần thực thi
ShowNum3();  //dòng lệnh 3 cần thực thi

Để chạy thử ví dụ này chúng ta hãy sử dùng Chrome, bật chế độ Debug lên bằng cách ấn F12. Rồi ở màn hình Development Mode, ta vào tab Console. Sau đó có thể paste code Javascript trực tiếp vào cửa sổ này và Enter để xem kết quả.

Chúng ta sẽ nhận được 3 giá trị hiển thị lần lượt là 1, 2, 3.

Javascript thường thì khi chạy code sẽ thực thi tuần tự hoặc còn gọi là thực thi đồng bộ (synchronous, viết tắt là sync). Tức là, các lệnh được thực thi lần lượt từ trên xuống dưới.

Hỏi ngu:Chạy tuần tự được, thế có chạy không tuần tự được không?

Được. Giờ nếu code Javascript chạy theo kiểu không tuần tự, hay còn gọi là bất đồng bộ (asynchronous, viết tắt là async) thì chúng ta sửa lại như sau:

function ShowNum1(){
	console.log('1');
}
function ShowNum2(){
	console.log('2');
}
function ShowNum3(){
	console.log('3');
}
ShowNum1();                      //dòng lệnh 1 cần thực thi
setTimeout(ShowNum2, 2000);      //dòng lệnh 2 cần thực thi
ShowNum3();                      //dòng lệnh 3 cần thực thi

Chúng ta sẽ nhận được 3 giá trị 1, 3, 2.

Hỏi ngu: Tại sao lại như vậy nhỉ? WHY ?!?

Dòng lệnh thứ 3 đã chạy trước rồi dòng thứ 2 mới chạy sau. Như vậy nghĩa là không tuần tự theo thứ tự hàm viết từ trên xuống nữa. Điều này xảy ra được là vì dòng lệnh thứ 2 chạy ngầm. Nó vẫn đang chạy và 2 giây sau nó trả về kết quả, còn dòng thứ 3 không phải đợi dòng 2 chạy xong rồi mới chạy. Mà gần như 2 dòng 2 và 3 chạy đồng thời (Ta gọi đó là chạy bất đồng bộ, hay là chạy kiểu async)

Hỏi ngu: Làm thế nào mà hàm setTimeout() lại chạy ngầm được?

Là vì Javascript có những hàm thực thi theo chế độ non-blocking (không chặn các hàm khác, hay không làm chương trình bị đơ). Con trỏ hàm sẽ chạy tiếp lệnh tiếp theo chứ toàn bộ chương trình không đợi từng lệnh được thực thi. Hàm console.log() cũng là một hàm non-blocking.

Hỏi ngu: Hàm non-blocking à, thế thì phải có cả hàm blocking chứ nhỉ?

Đúng vậy, Javascript có cả các hàm Blocking. Chúng ta hãy thử viết lại ví dụ lúc nãy. Trong ShowNum1() tôi thay hàm console.log() bằng hàm alert():

function ShowNum1(){
	alert('1');
}
function ShowNum2(){
	console.log('2');
}
function ShowNum3(){
	console.log('3');
}
ShowNum1();                      //dòng lệnh 1 cần thực thi
setTimeout(ShowNum2, 2000);      //dòng lệnh 2 cần thực thi
ShowNum3();                      //dòng lệnh 3 cần thực thi

Sau khi chạy thử ta thấy cửa sổ bật lên hiện ra số 1, trình duyệt cứ đơ ở đó và màn hình dòng lệnh không thấy chạy tiếp. Chỉ khi nhấn OK thì console mới hiện ra số 3, rồi số 2. Alert chính là một hàm Blocking I/O (chặn đầu ra đầu vào của chương trình thực thi javascript, ở đây có thể hiểu là trình duyệt Chrome bị chặn) dẫn đến Chrome được lệnh phải đợi lệnh gọi hàm alert() chạy xong mới đc chạy tiếp dòng lệnh tiếp theo.

Ghi chú: Đối với Javascript và NodeJS thì hầu hết các hàm đều là dưới dạng Non-Blocking (async) để không làm gián đoạn trải nghiệm của người dùng.

Hỏi ngu: Có vẻ như Blocking chính là Sync và Non-Blocking chính là Async?

Đúng vậy, bạn đã hiểu đúng vấn đề.

Hỏi ngu: Thế sao lại phải có 2 cách gọi làm gì nhỉ?

Vì gọi Async (Ây-Xuynh) và Sync (Xuynh) nghe nó sang mồm hơn 😝. Thật ra async và sync mô tả cách code và quản lý dữ liệu theo luồng tốt hơn blocking. Đó là 2 từ được phổ biến gần đây, vì thế bạn cũng có thể quên 2 từ blocking, non-blocking đi cũng được.

Function và Object (Hàm và Biến)

Hãy thử bê nguyên cả hàm ShowNum2() nhét vào hàm setTimeout(), thay vì truyền tên hàm vào:

function ShowNum1(){
	console.log('1');
}
function ShowNum3(){
	console.log('3');
}
ShowNum1();
setTimeout(function ShowNum2(){
	console.log('2');
}, 2000);
ShowNum3();

Code vẫn chạy. Chúng ta vẫn nhận được 3 giá trị 1, 3, 2.

Sau khi định nghĩa ra một hàm, bạn hoàn toàn có thể dùng hàm đó để truyền vào 1 hàm khác như là 1 biến chứa giá trị. Cái này là đặc sản của Javascript khiến cho cách viết code rất linh hoạt và ngắn gọn, nhưng đôi khi hơi khó hiểu.

Trong Javascript thì Functions là Objects (các hàm cũng có thể coi là các biến).

Lấy thêm ví dụ nữa về hàm hoạt động như là biến:

function ShowNum1(){
	console.log('1');
}
var Object1 = ShowNum1();  //Gán hàm cho biến
Object1;                          //Hiển thị kết quả chứa trong biến

Kết quả là hiện ra số 1

Note lại: Đối với Javascript, hàm có thể dùng như biến.

Hỏi ngu:Thế có thể coi biến là hàm được không?

Thử chạy lại ví dụ vừa nãy xem sao, thử gọi biến Object1() giống như gọi hàm xem sao.

Không được rồi. Biến Object1 chỉ là vùng nhớ để chứa dữ liệu sau khi hàm chạy xong và đổ kết quả vào. Biến Object1 không phải là 1 con trỏ, trỏ vào một hàm để mà gọi ra thực thi bằng 2 dấu "()" được.

Hãy thử viết như sau:

(function ShowNum1(){
	console.log('1');
})();

Viết như thế này là chỉ định nghĩa hàm, không gọi hàm làm sao nó chạy được nhỉ?

Ấy thế mà nó lại chạy ngon. Đó là tại vì ta viết tắt, gộp cả định nghĩa hàm và gọi hàm luôn. Cách viết này có tên gọi là Self-Executing Anonymous Functions.

First-class Functions

Trong lập trình hàm (Functional Programming) có một khái niệm là First-class Functions.

Từ khóa này không có gì quá xa lạ. Nó chính là từ dùng để mô tả một hàm mà dùng được như một biến. Hàm first-class chính là hàm có thể coi là biến.

Hỏi ngu: Vậy trong Javascript toàn bộ các hàm đều là first-class?

Yes!

Hỏi ngu: Có first-class Function, vậy có first-class Object không?

Có luôn. Tham khảo thêm ở đây: https://stackoverflow.com/questions/245192/what-are-first-class-objects

Anonymous Function

Chúng ta thử viết lại ví dụ bên trên rồi bằng cách xóa chữ ShowNum2 đi, coi như hàm đó không có tên:

function ShowNum1(){
	console.log('1');
}
function ShowNum3(){
	console.log('3');
}
ShowNum1();
setTimeout(function(){
	console.log('2');
}, 2000);
ShowNum3();

Và tất nhiên là code vẫn chạy.

Một đặc tính kỳ lạ nữa của Javascript đó là hàm không nhất thiết phải có tên, do đó chỉ cần truyền 1 hàm không tên (anonymous function) cho hàm setTimeout là đủ.

Hỏi ngu:Có thể bỏ luôn chữ function() kia đi được không? :kissing_smiling_eyes: cho nó gọn.

Nếu bỏ chữ function() đi thì nó chỉ còn setTimeout({console.log('2');}, 2000). Như vậy là bạn đang truyền 1 biến không có tên vào đầu hàm chứ không phải truyền hàm không tên cho hàm nữa. Lúc này thì 1 biến không thể chứa 1 hàm console.log() được nữa. Và sẽ bị báo lỗi.

Hỏi ngu:Không ý tớ là bỏ chữ function thôi. Giữ lại dấu "()' ý.

Thế thử nhé:

Viết thế này cũng không được. vì dấu "()" dùng để thực thi hàm (chạy hàm, execute hàm) chứ không phải dùng để "định nghĩa hàm". Nếu bạn thích viết ngắn gọn k dùng chữ function, bạn có thể dùng đến arrow function.

Arrow Function

cách viết arrow function như sau:

function ShowNum1(){
    console.log('1');
}
function ShowNum3(){
    console.log('3');
}
ShowNum1();
setTimeout(() => {
    console.log('2');
}, 2000);
ShowNum3();

Kết quả :

Như vậy là thay vì dùng chữ "function()" ta viết "() =>" là sẽ mô tả được 1 hàm không tên (anonymous function). Chú ý đây là cách viết code của phiên bản 6 của Javascript. Phải chạy trên trình duyệt bản tương thích mới chạy đc. Có thể Chrome chạy nhưng mang lên Safari hoặc IE 9, IE10 lại k chạy.

Hỏi ngu: Viết thế này thì hay đấy! Có thể dùng để khai báo một hàm bằng cách này rồi gán cho biến k?

Được. Thử xem:

var ShowNum1 = () => {
    console.log('1');
}
function ShowNum3(){
    console.log('3');
}
ShowNum1();
setTimeout(() => {
    console.log('2');
}, 2000);
ShowNum3();

Hỏi ngu:Viết thế này dị quá nhỉ?

Muốn dị hơn cũng đc. Bạn đọc và xem xem có hiểu cách chạy của hàm dưới đây k nhé.

(() => console.log('1');)()

Callback Function

Nhìn lại đoạn code này:

setTimeout(function ShowNum2(){
	console.log('2');
}, 2000);

Chúng ta thấy hàm setTimeout() nhận nguyên 1 hàm khác làm tham số. Chúng ta không cần viết hàm ShowNum2() ra ngoài rồi gọi ở hàm setTimeout(), mà ta vứt thẳng luôn vào đầu vào của hàm setTimeout(). Và 2 hàm vẫn chạy lần lượt.

Cách viết code này dĩ nhiên là được nhiều người dùng vì nó rất ngắn gọn.

Vì là quá nhiều người dùng và yêu cách viết này. Ta phải đặt cho nó 1 cái tên cho máu. Khi vứt một hàm vào làm tham số của một hàm khác như thế, chúng ta sẽ gọi hàm vứt vào đó là hàm callback. ShowNum2() là một hàm callback (Callback Function).

Ghi chú: Kể cả không đặt tên cho hàm vứt vào, thì hàm đó vẫn được gọi là hàm Callback đấy nhé.

Câu hỏi: Nếu vứt vào như thế thì hàm ShowNum2() chạy trước hay hàm setTimeout() chạy trước? Tất nhiên là hàm setTimeout chạy trước, trong lúc nó chạy thì hàm ShowNum2 phải đợi. Hàm setTimeout khi chạy không có tác dụng gì cả mà chỉ khiến hàm khác đợi 1 thời gian. 2 giây sau thì setTimeout chạy xong, lúc đó ShowNum2 mới chạy và thực thi code bên trong ruột của nó, đó là in ra số 2.

Câu hỏi: Vậy hàm callback phải đợi hàm gọi nó chạy xong rồi mới được chạy? Đúng vậy, hàm callback chính là một hàm hoạt động như 1 biến (như đã nói ở trên), khi hoạt động, nó sẽ đợi khi hàm gọi nó (hàm mẹ) chạy xong và đón nhận kết quả từ hàm mẹ. Lúc này Callback đóng vai trò như 1 biến nhận + xử lý dữ liệu trả về sau khi thực thi xong hàm mẹ.

Callback có thể hiểu là call me when you are done 😆

Hãy xem thử ví dụ tiếp theo

function SoSanh(n) { 
  return function NhoHon10() { return n < 10; };
}
console.log(SoSanh(9)());
console.log(SoSanh(10)());
console.log(SoSanh(11)());

Chúng ta thấy hàm NhoHon10() nằm bên trong hàm SoSanh(). Và kết quả sau khi thực thi hàm NhoHon10() được trả về cho hàm SoSanh() để hiển thị. Lúc này hàm NhoHon10() chính là một hàm callback.

Note lại: Một hàm được gọi là hàm callback trong 2 trường hợp:

  1. Nó được truyền vào một hàm khác và nhận giá trị từ hàm đó.
  2. Nó chạy bên trong hàm khác và trả về giá trị cho hàm mẹ đó.

Hỏi ngu:Tại sao lại viết là SoSanh(10)(), hình như thừa dấu "()" kìa!

Nếu gọi SoSanh(10) thì ta mới chỉ chạy hàm mẹ, hàm này trả về 1 con trỏ hàm, là 1 chuỗi f mô tả thân hàm thôi. Do đó khi chạy ta sẽ thấy như sau:

Muốn chạy hàm NhoHon10() bên trong thì ta phải gọi nó. Dấu "()" thứ 2 chính là dấu gọi hàm NhoHon10().

Hỏi ngu:Nếu gọi được như thế thì tớ có thể truyền được tham số cho hàm callback bên trong ah?

Đúng, truyền ngon lành. Hãy thử viểt như sau:

function SoSanh(n) { 
  return function NhoHon(m) { return n < m; };
}
console.log(SoSanh(9)(10));
console.log(SoSanh(10)(9));

true false

Hỏi ngu:Viết thế hơi khó hiểu, viết rõ hơn ra được không?

Được, lúc này ta có thể sử dụng tính năng hàm cũng là biến. Ta sẽ khai báo 1 biến chứa luôn cả cái hàm đó. Cái biến đó ta cũng gọi như gọi hàm cũng đc:

function SoSanh(n) { 
   return function NhoHon(m) { return n < m; };
}
var NhoHon10 = SoSanh(10);
console.log(NhoHon10(9));
console.log(NhoHon10(11));

true false

Hỏi ngu:Ớ, có gì đó sai sai. Rõ ràng bên trên vừa bảo biến không thể gọi như hàm mà? Sao ở đây biến NhoHon10 lại gọi thành hàm NhoHon10() được thế?

Ah uh, đó là vì biến NhoHon10 là 1 biến chứa nguyên 1 hàm, có thể gọi nó là 1 con trỏ hàm. Giờ nếu gọi lệnh thực thi "()" ta sẽ chạy hàm trong ruột biến đó, và lấy ra đc kết quả. Ta cũng truyền được giá trị vào ruột của biến đó, bằng cách viết NhoHon10(9).

Hỏi ngu tiếp: Sao lại phải chạy hàm thông qua hàm console.log(), sao không gọi thẳng luôn NhoHon10(9)?

Đúng, viết NhoHon10(9) không sai, nhưng nếu gọi hàm vài lần thì sao:

function SoSanh(n) { 
   return function NhoHon(m) { return n < m; };
}
var NhoHon10 = SoSanh(10);
NhoHon10(9);
NhoHon10(10);
NhoHon10(11);

Như ta thấy, 3 hàm được gọi mà chỉ có 1 kết quả show lên. Vậy tức là sao? Thì ra là do lệnh return n < m; là một lệnh làm hàm NhoHon bị blocking, hay chính là hàm sync. Nghĩa là nó chạy và đợi chương trình thao tác với nó, khiến các hàm phía sau không được chạy nữa. Hàm console.log() thì là async, nó không chặn mà cho phép các hàm phía sau chạy dồng thời với nó. Nếu thay bằng đoạn code sau thì sẽ chạy ngon lành:

function SoSanh(n) { 
   return function NhoHon(m) { console.log (n < m); };
}
var NhoHon10 = SoSanh(10);
NhoHon10(9);
NhoHon10(10);
NhoHon10(11);

true false false

Hỏi ngu:Thế Javascript thì hầu như hàm nào cũng là hàm callback à?

Không đúng, chúng ta vẫn có cách viết hàm gọi hàm bình thường, như sau:

var a = 1;
function Cong() {
  for (var i = 0; i < 10; i++) {
     a += i;
     HienThi(a);
  }
}
function HienThi(b) {
  console.log(b);
}
Cong();

Ở đây hàm Cong() gọi hàm HienThi() để hiển thị kết quả cho nó mỗi khi nó cần, mỗi khi nó cộng xong. Hàm HienThi() cần nhận 1 biến b, từ biến a truyền sang như bình thường.

Hỏi ngu:Thế ví dụ vừa rồi viết lại sang dạng dùng callback được không?

Được. Viết lại như sau:

var a = 1;
function Cong(callback) {
  for (var i = 0; i < 10; i++) {
     a += i;
     callback(a);
  }
};
function HienThi(b) {
  console.log(b);
};
Cong(HienThi);

Hỏi ngu: Viết như thế này thì giải thích cách hoạt động của nó như thế nào?

Rất đơn giản, hàm HienThi() chúng ta coi là 1 biến, và gắn biến đó cho biến calback. Bên trong vòng for, ta lại dùng biến callback như là 1 hàm bằng cách gọi callback(a). Javascript là như vậy, hàm là biến, biến là hàm.

Hỏi ngu:Viết như thế này thì loằng ngoằng quá nhỉ? có vẻ callback không có tác dụng gì nhiều!

Callback sinh ra tất nhiên là có tác dụng của nó. Hãy chạy thử ví dụ sau đây (Ví dụ này là cách viết cổ điển nhất của Ajax. Nếu muốn dùng $.ajax thì phải cần đến thư viện Jquery. Ở đây ta chỉ dùng Javascript thuần nên không muốn đụng đến Jquery):

function request(url) {             //Đây là hàm nhận đầu vào là 1 chuỗi url của API chứa data muốn lấy về
  const xhr = new XMLHttpRequest(); //Sử dụng Object XMLHttpRequest được viết sẵn của js để lấy data
  xhr.open("GET", url, false);      //Hàm này dùng để gọi đến url chứa data, tham số false nghĩa là muốn biến xhr này chạy sync. Các hàm khác phía sau phải đợi nó chạy xong mới đc chạy.
  xhr.send();                       //Gửi request đến url API cần lấy data.
  return xhr.responseText;          //Kết quả trả về sẽ chứa trong chuỗi responseText
}
console.log(request("https://jsonplaceholder.typicode.com/posts/1"));
console.log("1");

Hỏi ngu:Nếu muốn chương trình không bị đơ thì dòng 2 phải chạy song song trong lúc đợi dòng 1 chạy, tức là viết thành async à?

Đúng vậy, hãy thử chuyển thành Async bằng cách đặt tham số là true:

function request(url) {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', url, true)    //đổi tham số từ false (chạy sync) sang true (chạy async)
  xhr.send();
  return xhr.responseText;
}
console.log( request('https://jsonplaceholder.typicode.com/posts/1'));
console.log('1')

Vì chúng ta viết dạng async, tức là hàm open() đang chạy nhưng con trỏ đã chạy xuống return và trả về kết quả là "" (chuỗi rỗng).

**Hỏi ngu: **Vậy làm thế nào để lấy được kết quả từ server và in ra nếu viết kiểu async?

Chúng ta sử dụng hàm callback, viết như sau:

function XuLy(data) {
  console.log(data);
}
function request(url, HamCallBack) {
  const xhr = new XMLHttpRequest();
  xhr.onreadystatechange = function Success() {
    HamCallBack(xhr.responseText);
  };
  xhr.open("GET", url, true);
  xhr.send();
}
request("https://jsonplaceholder.typicode.com/posts/1", XuLy);
console.log("1");

Callback-Hell, Higher Order Function, Promises, Await, ...

Vì bài này đã khá dài và đọc mỏi mắt rồi nên tôi sẽ cắt phần sau sang một bài khác ở cùng Series này nhé, thanks các bạn đã đọc, hãy feedback vào comment nếu có chỗ nào bạn thắc mắc nhé!

Mời các bạn đọc phần 2: https://viblo.asia/p/tu-javascript-thuan-den-rxjs-phan-2-Ljy5Vx2GZra