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?
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
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.
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ó value và done" đượ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 done là true, 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();
}
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...of | Biến nhận được ở mỗi lượt | Ghi 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 |
| Map | Mảng 2 phần tử [key, value] | Các cặp được lấy ra theo thứ tự đã thêm vào |
| Generator | Các giá trị viết trong yield | Bả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 |
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
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ó.
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)
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.
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
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?