Học bằng cách đọc theo thứ tự

Iterator và generator — Lấy ra từng giá trị một

Iterator trả về từng giá trị một, generator dừng lại ở mỗi yield. Bài viết gồm next(), for...of, spread và cách chỉ lấy đủ số giá trị cần từ một dãy vô tận.

Khi phát số thứ tự từng vé một, hay hiển thị một danh sách review dài thành từng trang vài review, nếu tạo sẵn toàn bộ số hoặc trang ngay từ đầu, bạn sẽ chuẩn bị cả những phần không bao giờ được dùng. Bạn có thể viết một hàm dùng closure để nhớ giá trị kế tiếp, nhưng khi đó bạn phải tự theo dõi đã đi tới đâu và đã kết thúc hay chưa.

Bài này nói về iterator, thứ trả về từng giá trị một, và generator, cho phép bạn viết iterator dưới dạng hàm.

Hàm chạy tiếp từ chỗ đã dừng — Generator và yield

Giả sử bạn muốn có một hàm mỗi lần gọi thì trả về số thứ tự kế tiếp ở quầy giao dịch: 101, rồi 102, rồi 103. Hàm bình thường kết thúc ngay khi chạy return, và lần gọi sau lại chạy từ đầu, nên bạn cần một biến bên ngoài để nhớ đã trả tới số nào.

Khi gọi một generator function (hàm được khai báo bằng function*, bên trong dùng được yield), thân hàm không chạy; thay vào đó bạn nhận về một generator (object cho phép thân hàm chạy từng đoạn một). Mỗi lần gọi next() của nó, thân hàm chạy tới yield kế tiếp rồi dừng lại. next() trả về một object, trong đó value chứa giá trị được yield, còn done cho biết thân hàm đã chạy xong hay chưa.

// Khai báo bằng function* sẽ tạo ra một generator function
function* callNumbers() {
  console.log("Quầy bắt đầu nhận khách");
  yield 101;
  yield 102;
  yield 103;
}

// Gọi hàm thì thân hàm chưa chạy, chỉ trả về một generator
const numbers = callNumbers();

// Mỗi lần gọi next() thì chạy tới yield kế tiếp rồi dừng
const first = numbers.next();                   // "Quầy bắt đầu nhận khách" được in ra ở đây
console.log(`${first.value} / ${first.done}`);  // 101 / false
console.log(numbers.next().value);              // 102
console.log(numbers.next().value);              // 103

// Khi không còn yield nào, done thành true
const last = numbers.next();
console.log(`${last.value} / ${last.done}`);    // undefined / true
Mỗi lần next() đẩy điểm dừng tiến lên
numbers =callNumbers()Thân hàm chưachạy dòng nàonext()lần 1In dòng chữ rồidừng ở yield 101next()lần 2 và 3Dừng ở yield 102rồi yield 103next()lần 4value: undefineddone: true
"Quầy bắt đầu nhận khách" được in ra ở lần next() đầu tiên, rồi hàm dừng ở yield 101. Mỗi lần next() chạy tới yield kế tiếp.

Khi thân hàm chạy tới cuối hoặc chạy return, done thành true, và các lần gọi next() sau đó không chạy thân hàm nữa. Chính generator ghi nhớ nó đã đi tới đâu, nên bạn không cần biến bên ngoài để nhớ số thứ tự của quầy.

Không viết yield bên trong callback được

Bạn chỉ viết được yield bên trong cặp ngoặc nhọn của hàm khai báo bằng function*. Arrow function truyền cho forEach là một hàm khác, nên viết yield ở đó sẽ gây SyntaxError — và thông báo lỗi không hề nhắc tới yield. Muốn yield từng phần tử của mảng, hãy viết yield trong thân vòng lặp for hoặc for...of.

Phát số vé đúng bằng số lượng đã chuẩn bị. ticketCount đã được khai báo sẵn.

① Hãy biến issueTickets thành generator function yield các số từ 1 đến count.

② Sau khi trả hết các số, hãy return "Đã ngừng phát vé".

③ Hãy truyền ticketCount vào để nhận về một generator.

④ Hãy lấy giá trị tiếp theo 4 lần, in value và done theo dạng "1 / false".

(Nếu chạy đúng, phần giải thích sẽ xuất hiện.)

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Lặp mà không cần viết next() — Iterator protocol

Giả sử bạn muốn hiển thị trạng thái giao hàng theo thứ tự "Đã tiếp nhận", "Đã xuất kho", "Đang giao". Nếu viết mỗi bước một lời gọi next(), có bao nhiêu bước là bấy nhiêu dòng, và mỗi khi thêm bước bạn lại phải viết thêm. Dùng while để lặp thì bạn vẫn phải tự viết phần kiểm tra done và lời gọi next() kế tiếp.

Quy ước "mỗi lần gọi next() trả về một object có valuedone" được gọi là iterator protocol, và object tuân theo quy ước này là một iterator. Generator ở phần trước cũng là iterator. for...of gọi next() liên tục cho đến khi donetrue, và đưa từng value vào biến của vòng lặp.

function* deliverySteps() {
  yield "Đã tiếp nhận";
  yield "Đã xuất kho";
  yield "Đang giao";
}

// for...of gọi next() cho đến khi done là true
for (const step of deliverySteps()) {
  console.log(step);            // 3 dòng: Đã tiếp nhận / Đã xuất kho / Đang giao
}

// Cùng cách chạy đó, viết bằng while và next()
const steps = deliverySteps();
let result = steps.next();
while (!result.done) {
  console.log(result.value);    // 3 dòng: Đã tiếp nhận / Đã xuất kho / Đang giao
  result = steps.next();
}
for...of làm sẵn những gì cho bạn
Lặp bằngfor...ofTự độnggọi next()Tự độngkiểm tra doneĐưa value vào steprồi chạy thân lặpTự lặpbằng whileresult =steps.next()Tự kiểm tra!result.doneDùng result.valuetrong thân lặp
Cả hai đều lặp lại cột 2–4 cho đến khi done là true. Với for...of, bạn không cần viết next() hay phần kiểm tra done.

Bạn có thể truyền cho for...of bất kỳ giá trị iterable nào (giá trị mà for...of lấy được iterator từ nó), như mảng, Map hay generator. for...of không phân biệt loại giá trị; nó chỉ gọi next() của iterator lấy được, nên bạn lặp qua tất cả theo cùng một cách. Bảng dưới đây cho biết biến của vòng lặp nhận giá trị gì.

Giá trị truyền cho for...ofBiến nhận được ở mỗi lượtGhi chú
Mảng ["Đã tiếp nhận", "Đã xuất kho"]Từng phần tử ("Đã tiếp nhận" → "Đã xuất kho")Mỗi vòng for...of lấy một iterator mới, nên luôn bắt đầu từ phần tử đầu
MapMảng 2 phần tử [key, value]Các cặp được lấy ra theo thứ tự đã thêm vào
GeneratorCác giá trị viết trong yieldBản thân nó là iterator, nên vị trí đã đi tới bằng next() vẫn được giữ nguyên
Object như { name: "Bút" }Không nhận được gì (TypeError: … is not iterable)Object không phải iterable, nên muốn duyệt các key hãy dùng for...in hoặc Object.entries

Chia review sản phẩm thành các trang, mỗi trang 2 review, rồi in ra. reviews đã được khai báo sẵn.

① Trong paginate, hãy yield lần lượt từng trang dưới dạng mảng, bắt đầu từ đầu danh sách.

② Hãy lặp qua các trang của reviews, mỗi trang 2 review.

③ Với mỗi trang, hãy in ra "Trang 1: " rồi đến tên người viết review, nối với nhau bằng ", ".

④ Cuối cùng, hãy in ra số trang theo dạng "Tổng cộng 3 trang".

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Chuyển thành mảng — Cú pháp spread và chỉ dùng được một lần

Giả sử bạn muốn hiển thị các khung giờ lấy hàng trên một dòng, như "10:00 / 13:00 / 16:00". Generator không phải mảng, nên không có join hay length, và tự nó thì bạn cũng không đếm được có bao nhiêu giá trị. Dùng for...of để push từng giá trị vào mảng thì lại phải viết thêm vài dòng chỉ cho việc đó.

Nếu đặt generator vào trong cặp ngoặc vuông của mảng kèm cú pháp spread ..., nó lấy giá trị ra cho đến khi done là true và xếp theo thứ tự vào một mảng mới. Chỉ cần viết const slotList = [...slots];, sau đó bạn dùng được cả join lẫn length.

function* pickupSlots() {
  yield "10:00";
  yield "13:00";
  yield "16:00";
}

// Generator không phải mảng, nên không có length
const slots = pickupSlots();
console.log(slots.length);                // undefined

// Cú pháp spread trong [] xếp các giá trị theo thứ tự
const slotList = [...slots];
console.log(slotList.join(" / "));        // 10:00 / 13:00 / 16:00

// Spread lại một generator đã dùng hết thì được mảng rỗng
console.log([...slots].length);           // 0

// Gọi lại hàm thì được một generator mới, bắt đầu lại từ đầu
console.log([...pickupSlots()].length);   // 3
Spread lần thứ hai cho ra mảng rỗng
[...slots]lần 1Đủ 3 khung giờ10:00 đến 16:00[...slots]lần 2Mảng rỗnglength là 0Gọi lại[...pickupSlots()]Đọc lại từ đầulength là 3
[...slots] lần thứ hai bắt đầu đọc từ cuối, nơi lần thứ nhất đã dừng. Chỉ generator được gọi lại mới bắt đầu từ đầu.

Khi cần dùng cùng các giá trị ở hai chỗ trở lên, hãy spread chúng thành mảng một lần, lưu vào biến rồi dùng lại mảng đó. Generator đã được for...of lặp tới cuối cũng cho ra mảng rỗng nếu sau đó bạn spread nó.

Liệt kê mã thành viên của các đơn đăng ký thành viên đã được duyệt. applications đã được khai báo sẵn.

① Hãy viết một generator function chỉ chọn các đơn đã được duyệt và trả về từng mã thành viên một.

② Hãy nhận một generator, chỉ lấy ra mã đầu tiên và in theo dạng "Đầu tiên: M-4102".

③ Hãy gom phần còn lại vào một mảng và in theo dạng "Còn lại 2: M-4104, M-4105".

④ Hãy in ra tất cả các mã, kể cả mã đầu tiên, nối với nhau bằng ", ".

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Chỉ tạo những gì cần dùng — Lazy evaluation và dãy vô tận

Khi đánh số thứ tự cho các thông báo bắt đầu từ 1, bạn không biết trước sẽ có bao nhiêu thông báo. Nếu tạo sẵn 1000 số trong một mảng, dù chỉ dùng 3 số thì phần còn lại vẫn bị tạo ra vô ích. Còn nếu không đặt giới hạn trên, bạn sẽ không bao giờ tạo xong mảng đó.

Generator cho phép bạn viết theo kiểu lazy evaluation (chỉ tính một giá trị vào đúng lúc cần đến nó). Kể cả một hàm yield mãi mãi bằng while (true), thân hàm cũng chỉ chạy đến mức mà các lần gọi next() yêu cầu. Destructuring mảng cũng dùng được với generator, và lấy ra đúng số giá trị bằng số biến bạn viết ở vế trái.

function* serialFrom(start) {
  let number = start;
  while (true) {
    console.log(`Đã tạo ${number}`);
    yield number;
    number++;
  }
}

// Destructuring lấy ra đúng số giá trị bằng số biến ở vế trái
const [first, second] = serialFrom(1);   // Đã tạo 1 / Đã tạo 2
console.log(`${first}, ${second}`);       // 1, 2

// for...of ngừng lấy giá trị khi bạn break ra khỏi vòng lặp
for (const number of serialFrom(1)) {
  console.log(`Thông báo số ${number}`);
  if (number === 3) break;
}
// Đã tạo 1 / Thông báo số 1 / Đã tạo 2 / Thông báo số 2
// Đã tạo 3 / Thông báo số 3 (số 4 không bao giờ được tạo)
Cách lấy giá trị quyết định next() chạy bao nhiêu lần
serialFrom(1)dãy số vô tậnDestructuring bằngconst [a, b] =for...of in 3 sốrồi break[...serialFrom(1)]để tạo mảngnext() đượcgọi 2 lầnnext() đượcgọi 3 lầndone không baogiờ thành trueChỉ tạo 1 và 2rồi kết thúcTạo từ 1 đến 3rồi thoát vòng lặpGọi next() mãikhông bao giờ xong
Hai cột bên trái dừng lại vì số giá trị cần lấy đã cố định. Cú pháp spread đọc cho đến khi done là true, nên không dùng được với dãy vô tận.

Muốn biến một dãy vô tận thành mảng, hãy đặt thêm một generator khác ở giữa, lấy đủ một số lượng giá trị nhất định rồi return. Khi generator ở giữa đó kết thúc, done thành true, nên cú pháp spread cũng dừng lại ở đó.

Không tới được yield thì next() không bao giờ trả về

Nếu bạn viết vòng lặp while (true) chỉ yield khi điều kiện if đúng, mà điều kiện đó không bao giờ đúng, thì next() sẽ không bao giờ trả về. Chương trình chạy mãi không xong, và các dòng bên dưới không bao giờ được chạy. Khi lặp vô tận, trước hết hãy chắc chắn rằng sớm muộn gì cũng có giá trị thỏa điều kiện.

Tính thời gian chờ trước mỗi lần thử lại sau khi một request mạng thất bại, mỗi lần gấp đôi. retryDelays đã được khai báo sẵn và yield giá trị mãi mãi.

① Hãy cho take chỉ yield count giá trị đầu tiên từ source, rồi kết thúc.

② Bắt đầu từ 100, hãy đưa 4 giá trị đầu tiên vào một mảng và in ra, nối bằng " → ".

③ Hãy dùng vòng lặp lấy 5 giá trị đầu tiên và in tổng theo dạng "Tổng: 3100ms".

④ Hãy truyền 0 làm số lượng, chuyển kết quả thành mảng rồi in ra length của nó.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra
QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

Câu 1Ngay sau khi gọi một hàm khai báo bằng function* và nhận về generator, thân hàm đã chạy tới đâu?

Câu 2Với generator slots yield 3 giá trị, nếu chạy [...slots] hai lần liên tiếp thì length ở lần thứ hai là bao nhiêu?

Câu 3Với generator gen yield mãi mãi bằng while (true), cách nào dưới đây không bao giờ kết thúc?