Vòng lặp và callback

Sử dụng vòng lặp với callback

Ở bài này sẽ không giới thiệu Promise, nhưng có giải thích kỹ hơn về hàm bất đồng bộ, cùng với scope của biến.
Khi sử dụng vòng lặp và callback lấy giá trị từ vòng lặp thì nên sử dụng cẩn thận vì có thể gặp một số lỗi không mong muốn.

Ví dụ muốn in các biến đếm trong vòng lặp for trong callback của hàm setTimeout:

1
2
3
4
5
for (var i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, 0);
}

Để ý là hàm callback là closure (hàm lấy giá trị từ bên ngoài hàm). Cùng với đó là hàm callback cũng là hàm bất đồng bộ.

Kết quả sẽ in ra là năm số năm 5, 5, 5, 5, 5.
Lý do cho kết quả trên là hàm bất đồng bộ trong setTimeout sẽ chạy sau khi vòng for được chạy. Hàm callback này sẽ lấy giá trị biến i ở ngoài hàm, và biến này chỉ được khởi tạo một lần duy nhất.

Ví dụ ở trên có thể viết lại:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/* hàm đồng bộ được thực thi */

// biến i khai báo là var nên có scope là hàm chứa nó
// hoặc scope là window nếu không có hàm nào chứa nó
for (var i = 0; i < 5; i++) {
}

/* sau đây các hàm bất đồng bộ được thực thi */
(function() { // thực thi hàm callback
  console.log(i); // -> 5
})();
// câu lệnh trên cũng tương đương với
console.log(i); // -> 5
console.log(i); // -> 5
console.log(i); // -> 5
console.log(i); // -> 5

Sửa lỗi hàm callback lấy giá trị biến đếm của vòng for

Nếu muốn ví dụ ở trên in được ra giá trị từ 0 đến 4 thì chúng ta có thể sửa lại bằng cách tạo ra mỗi biến khác nhau trong hàm callback.

Đầu tiên là sử dụng let, khi sử dụng với từ khóa let thì một biến mới được tạo ra. Trong vòng lặp for lúc này sẽ có 5 biến mới được tạo ra.

1
2
3
4
5
6
7
for (var i = 0; i < 5; i++) {
  let j = i; // tạo biến mới trong từng lần lặp
  setTimeout(function() {
    console.log(j);
  }, 0);
}
// -> 0, 1, 2, 3, 4

Hay có thể viết ngắn gọn thành:

1
2
3
4
5
6
7
// biến i sẽ tạo mới trong từng lần lặp
for (let i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, 0);
}
// -> 0, 1, 2, 3, 4

Truyền biến vào tham số hàm callback

Chúng ta có thể sửa lại ví dụ đầu tiên bằng cách truyền tham số vào hàm callback.

1
2
3
4
5
6
7
8
9
// biến i sẽ nhận là tham số hàm callback
// vì sẽ tạo ra 5 hàm callback khác nhau
// mỗi hàm callback sẽ nhận các tham số khác nhau
for (var i = 0; i < 5; i++) {
  setTimeout(function(j) {
    console.log(j);
  }, 0, i);
}
// -> 0, 1, 2, 3, 4

Tạo ra các closure

Ví dụ này khác ví dụ trước là tạo ra các closure, trong đó từng closure sẽ tạo ra các biến khác nhau.
Closure là hàm có thể sử dụng lại các biến ở bên ngoài hàm, ngay cả khi hàm ở ngoài đã thực thi xong.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// tạo ra 5 hàm không tên và thực thi nó ngay lập tức
// mỗi hàm không tên thì lưu một giá trị biến j riêng
for (var i = 0; i < 5; i++) {
  (function() {
    var j = i; // biến j thuộc hàm không tên
    setTimeout(function() {
      console.log(j);
    }, 0);
  })();
}
// -> 0, 1, 2, 3, 4

Còn một cách nữa cũng có thể tạo ra hàm không tên ở trong callback.
Ví dụ sau sẽ tạo hàm không tên trong callback và thực thi nó ngay lập tức. Mỗi hàm sẽ nhận các tham số khác nhau.

1
2
3
4
5
// hàm không tên được viết dưới dạng hàm mũi tên
for (var i = 0; i < 5; i++) {
  setTimeout((j => () => console.log(j))(i), 0);
}
// -> 0, 1, 2, 3, 4