JavaScript Generators & yield — A Practical Guide with 10 Use Cases
A bilingual deep-dive into generator functions and yield: the pausable-function mental model, core syntax, two-way communication, async generators, plus 10 real-world use cases and practice exercises.
Mô hình tư duy — Hàm có thể tạm dừng
Hàm thường chạy đến khi xong: bạn gọi, nó chạy, nó return một lần. Generator function thì khác — nó có thể tạm dừng giữa chừng, trả về một giá trị, rồi tiếp tục sau.
Hãy nghĩ nó như video bạn có thể tạm dừng: yield là nút pause, và .next() là nút play.
function* counter() {
console.log("start");
yield 1; // pause here, give back 1 {dừng ở đây, trả về 1}
console.log("resumed");
yield 2; // pause again, give back 2 {dừng tiếp, trả về 2}
console.log("done");
}
const gen = counter(); // nothing runs yet {chưa chạy gì cả}
gen.next(); // logs "start", returns { value: 1, done: false }
gen.next(); // logs "resumed", returns { value: 2, done: false }
gen.next(); // logs "done", returns { value: undefined, done: true }
Hai điều khiến generator đặc biệt:
- Thực thi lười: code không chạy đến khi bạn yêu cầu giá trị tiếp theo.
- Trạng thái được giữ: biến cục bộ tồn tại giữa các lần dừng.
Cú pháp cơ bản
Khai báo & Gọi
// The asterisk * makes it a generator {Dấu * biến nó thành generator}
function* myGen() {
yield "a";
yield "b";
}
// Calling returns an iterator — it does NOT execute the body
// {Gọi trả về một iterator — KHÔNG thực thi thân hàm}
const it = myGen();
.next() và đối tượng kết quả
Mỗi lần gọi .next() trả về một object:
const it = myGen();
it.next(); // { value: "a", done: false }
it.next(); // { value: "b", done: false }
it.next(); // { value: undefined, done: true } {hết rồi}
- thứ mà
yieldtrả về falsekhi đang dừng,truekhi xong
Tiêu thụ bằng for...of và spread
Bạn hiếm khi gọi .next() thủ công. Generator là iterable, nên dùng các công cụ bạn đã biết:
function* fruits() {
yield "apple";
yield "banana";
yield "cherry";
}
// for...of automatically calls .next() until done
// {for...of tự động gọi .next() đến khi done}
for (const f of fruits()) {
console.log(f); // apple, banana, cherry
}
// Spread collects all yielded values {Spread gom mọi giá trị yield}
const arr = [...fruits()]; // ["apple", "banana", "cherry"]
// Destructuring works too {Destructuring cũng được}
const [first, second] = fruits(); // first="apple", second="banana"
Lưu ý:
for...ofvà spread bỏ qua giá trịreturncuối và dừng ởdone: true.
Cơ chế nâng cao
Giao tiếp hai chiều — truyền giá trị vào next()
Đây là tính năng bị hiểu lầm nhiều nhất. yield không chỉ là đầu ra — nó còn là đầu vào. Bất cứ gì bạn truyền vào .next(x) trở thành kết quả của biểu thức yield đang bị dừng:
function* conversation() {
const name = yield "What's your name?";
const age = yield `Hello ${name}, how old are you?`;
return `${name} is ${age} years old`;
}
const chat = conversation();
chat.next(); // { value: "What's your name?", done: false }
chat.next("Vinix"); // "Vinix" becomes the value of the first yield
// { value: "Hello Vinix, how old are you?", done: false }
chat.next(28); // 28 becomes the value of the second yield
// { value: "Vinix is 28 years old", done: true }
Điểm mấu chốt: tham số của lần .next() đầu tiên luôn bị bỏ qua vì chưa có yield nào đang dừng để nhận nó.
yield* — Uỷ quyền cho iterable khác
yield* uỷ quyền việc lặp cho generator/iterable khác:
function* inner() {
yield 2;
yield 3;
}
function* outer() {
yield 1;
yield* inner(); // delegate — yields 2, then 3 {uỷ quyền — yield 2, rồi 3}
yield 4;
}
console.log([...outer()]); // [1, 2, 3, 4]
// Works with any iterable {Hoạt động với mọi iterable}
function* spreadString() {
yield* "hi"; // yields "h", "i"
yield* [10, 20]; // yields 10, 20
}
console.log([...spreadString()]); // ["h", "i", 10, 20]
Đây là nền tảng cho duyệt đệ quy (xem ví dụ cây bên dưới).
return và .throw()
function* withReturn() {
yield 1;
return 99; // sets done:true with value 99 {đặt done:true với value 99}
yield 2; // never reached {không bao giờ tới}
}
const g = withReturn();
g.next(); // { value: 1, done: false }
g.next(); // { value: 99, done: true }
g.next(); // { value: undefined, done: true }
// .throw() injects an error at the paused yield {.throw() ném lỗi tại yield đang dừng}
function* safe() {
try {
yield "working";
} catch (e) {
console.log("caught:", e); // caught: oops
yield "recovered";
}
}
const s = safe();
s.next(); // { value: "working", done: false }
s.throw("oops"); // logs "caught: oops", { value: "recovered", done: false }
Async Generator — for await...of
Kết hợp async + function* để yield promise theo thời gian:
async function* fetchPages() {
let url = "/api/items?page=1";
while (url) {
const res = await fetch(url);
const data = await res.json();
yield data.items; // yield each page's items {yield items mỗi trang}
url = data.nextPage; // null when no more pages {null khi hết trang}
}
}
// Consume with for await...of {Tiêu thụ bằng for await...of}
for await (const items of fetchPages()) {
renderItems(items);
}
Demo trực tiếp
Lý thuyết “thông” nhanh hơn khi bạn nhìn thấy nó. Demo này cho bạn: bước generator bằng .next() và xem nó dừng ở mỗi yield, theo dõi pipeline lười filter → map → take từng giá trị, và cho vòng lặp blocking đua với cooperative scheduling để thấy generator giữ UI mượt thế nào.
Mở demo đầy đủ:
10 Use Case thực tế
1. Chuỗi vô hạn & ID duy nhất
Generator có thể vô hạn vì nó chỉ tính khi cần:
function* idGenerator(prefix = "id") {
let n = 1;
while (true) {
yield `${prefix}_${n++}`;
}
}
const ids = idGenerator("user");
ids.next().value; // "user_1"
ids.next().value; // "user_2"
ids.next().value; // "user_3"
Use case: key duy nhất ổn định cho element tạo động, mà không cần biến đếm toàn cục rò rỉ khắp nơi.
2. Range lười
Python có range(); JavaScript thì không — nhưng generator cho bạn một cái:
function* range(start, end, step = 1) {
for (let i = start; i < end; i += step) {
yield i;
}
}
console.log([...range(0, 5)]); // [0, 1, 2, 3, 4]
console.log([...range(0, 10, 2)]); // [0, 2, 4, 6, 8]
for (const i of range(1, 4)) {
console.log(i); // 1, 2, 3
}
Khác với tạo mảng, range(0, 1_000_000) dùng gần như không tốn bộ nhớ cho đến khi bạn thực sự lặp.
3. Iterable tuỳ chỉnh với Symbol.iterator
Cho class của bạn hoạt động với for...of và spread:
class LinkedList {
constructor() {
this.head = null;
}
add(value) {
this.head = { value, next: this.head };
return this;
}
// A generator method IS the iterator {Một method generator CHÍNH LÀ iterator}
*[Symbol.iterator]() {
let node = this.head;
while (node) {
yield node.value;
node = node.next;
}
}
}
const list = new LinkedList().add(1).add(2).add(3);
console.log([...list]); // [3, 2, 1]
for (const v of list) console.log(v); // 3, 2, 1
4. Pipeline lười — map / filter / take
Xử lý stream vô hạn hoặc khổng lồ mà không tạo mảng trung gian:
function* map(iterable, fn) {
for (const x of iterable) yield fn(x);
}
function* filter(iterable, predicate) {
for (const x of iterable) if (predicate(x)) yield x;
}
function* take(iterable, n) {
let count = 0;
for (const x of iterable) {
if (count++ >= n) return;
yield x;
}
}
// Compose lazily over an INFINITE sequence {Kết hợp lười trên chuỗi VÔ HẠN}
function* naturals() {
let n = 1;
while (true) yield n++;
}
const result = [
...take(
map(
filter(naturals(), (x) => x % 2 === 0), // even numbers {số chẵn}
(x) => x * x // square them {bình phương}
),
5 // only first 5 {chỉ 5 cái đầu}
),
];
console.log(result); // [4, 16, 36, 64, 100]
Mỗi giá trị chảy qua toàn bộ pipeline từng cái một — không bao giờ tạo mảng tất cả số chẵn.
5. Duyệt cây / graph với yield*
yield* đệ quy khiến duyệt theo chiều sâu cực dễ:
const tree = {
value: 1,
children: [
{ value: 2, children: [{ value: 4, children: [] }] },
{ value: 3, children: [] },
],
};
function* walk(node) {
yield node.value;
for (const child of node.children) {
yield* walk(child); // delegate to the recursive call {uỷ quyền cho lời gọi đệ quy}
}
}
console.log([...walk(tree)]); // [1, 2, 4, 3]
// Find the first matching node lazily — stops as soon as found
// {Tìm node khớp đầu tiên một cách lười — dừng ngay khi tìm thấy}
function findFirst(node, predicate) {
for (const value of walk(node)) {
if (predicate(value)) return value;
}
}
console.log(findFirst(tree, (v) => v > 2)); // 4
6. Máy trạng thái
Trạng thái được giữ của generator hoàn hảo để mô hình hoá các bước:
function* trafficLight() {
while (true) {
yield "green";
yield "yellow";
yield "red";
}
}
const light = trafficLight();
light.next().value; // "green"
light.next().value; // "yellow"
light.next().value; // "red"
light.next().value; // "green" (loops {lặp lại})
// A checkout wizard {Trình hướng dẫn thanh toán}
function* checkoutFlow() {
const cart = yield "show-cart";
const address = yield "enter-address";
const payment = yield "enter-payment";
return { cart, address, payment };
}
7. Phân trang với Async Generator
Giấu logic phân trang sau một iterator gọn gàng:
async function* paginate(endpoint) {
let page = 1;
while (true) {
const res = await fetch(`${endpoint}?page=${page}`);
const { items, hasMore } = await res.json();
yield* items; // yield each item individually {yield từng item}
if (!hasMore) break;
page++;
}
}
// The consumer doesn't know or care about pages
// {Bên tiêu thụ không biết và không cần quan tâm tới trang}
let count = 0;
for await (const item of paginate("/api/products")) {
process(item);
if (++count >= 100) break; // stop early — no extra fetches {dừng sớm — không fetch thừa}
}
8. Stream từng khối (Fetch Reader)
Đọc response lớn từng phần thay vì buffer toàn bộ:
async function* streamLines(url) {
const res = await fetch(url);
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let newlineIndex;
while ((newlineIndex = buffer.indexOf("\n")) >= 0) {
yield buffer.slice(0, newlineIndex);
buffer = buffer.slice(newlineIndex + 1);
}
}
if (buffer) yield buffer; // last line {dòng cuối}
}
// Process a huge log file line-by-line {Xử lý file log khổng lồ từng dòng}
for await (const line of streamLines("/logs/huge.txt")) {
if (line.includes("ERROR")) console.log(line);
}
9. Coroutine hai chiều (kiểu redux-saga)
Generator cho phép một “runner” điều khiển side effect trong khi generator vẫn thuần khiết. Đây chính xác là cách redux-saga hoạt động:
// Effects are plain objects — easy to test {Effect là object thuần — dễ test}
const call = (fn, ...args) => ({ type: "CALL", fn, args });
function* loginSaga() {
const user = yield call(fetchUser, 1); // describe an effect {mô tả một effect}
const posts = yield call(fetchPosts, user.id);
return { user, posts };
}
// The runner interprets effects and feeds results back in
// {Runner diễn giải effect và đưa kết quả trở lại}
async function runSaga(saga) {
const it = saga();
let result = it.next();
while (!result.done) {
const effect = result.value;
if (effect.type === "CALL") {
const value = await effect.fn(...effect.args);
result = it.next(value); // feed the result back {đưa kết quả trở lại}
}
}
return result.value;
}
Generator mô tả cái gì cần làm; runner quyết định làm thế nào. Sự tách biệt này khiến logic cực dễ test.
10. Lập lịch hợp tác — chia nhỏ vòng lặp nặng
Một vòng lặp đồng bộ dài làm đơ UI. Generator cho phép bạn xử lý từng khối và trả quyền điều khiển lại cho browser:
function* processInChunks(items, chunkSize = 500) {
for (let i = 0; i < items.length; i += chunkSize) {
const chunk = items.slice(i, i + chunkSize);
for (const item of chunk) heavyWork(item);
yield i + chunk.length; // progress so far {tiến độ hiện tại}
}
}
function runCooperatively(items) {
const it = processInChunks(items);
function step() {
const { value, done } = it.next();
if (done) return;
updateProgressBar(value / items.length);
// Hand control back so the browser can paint/respond
// {Trả quyền điều khiển để browser có thể paint/phản hồi}
requestIdleCallback(step);
}
step();
}
runCooperatively(hugeArray); // UI stays responsive {UI vẫn mượt}
Pattern thưởng thêm
Ghép hai iterable
function* zip(a, b) {
const itA = a[Symbol.iterator]();
const itB = b[Symbol.iterator]();
while (true) {
const x = itA.next();
const y = itB.next();
if (x.done || y.done) return;
yield [x.value, y.value];
}
}
console.log([...zip(["a", "b", "c"], [1, 2, 3])]);
// [["a", 1], ["b", 2], ["c", 3]]
Giới hạn luồng sự kiện
async function* throttle(asyncIterable, ms) {
let last = 0;
for await (const value of asyncIterable) {
const now = Date.now();
if (now - last >= ms) {
last = now;
yield value;
}
}
}
Các lỗi thường gặp
| Lỗi | Chi tiết |
|---|---|
| lặp một lần | Sau khi cạn, for...of không trả gì — gọi lại hàm để có cái mới |
| Không truy cập ngẫu nhiên | Không thể gen[5] — chỉ tuần tự .next() |
.next(arg) đầu bỏ qua arg | Chưa có yield đang dừng để nhận |
return trong for...of bị bỏ qua | Dùng .next() thủ công nếu cần giá trị return |
| Trộn sync/async | for...of không await — dùng for await...of |
| Quên dọn dẹp | break sớm kích hoạt khối finally — đặt dọn dẹp ở đó |
// finally runs even when the consumer breaks early
// {finally chạy ngay cả khi bên tiêu thụ break sớm}
function* withCleanup() {
try {
yield 1;
yield 2;
yield 3;
} finally {
console.log("cleanup!"); // closes files, sockets, etc. {đóng file, socket...}
}
}
for (const x of withCleanup()) {
if (x === 2) break; // logs "cleanup!" {vẫn chạy "cleanup!"}
}
Tham khảo nhanh
function* fn() {} → declare a generator {khai báo generator}
yield x → pause, output x {dừng, xuất x}
const v = yield x → pause; v = value passed to next() {v = giá trị truyền vào next()}
yield* iterable → delegate to another iterable {uỷ quyền}
gen.next(v) → resume, v becomes the yield result {tiếp tục}
gen.return(v) → force-finish with value v {kết thúc cưỡng bức}
gen.throw(e) → inject error at current yield {ném lỗi}
for...of → consume sync generator {tiêu thụ generator đồng bộ}
for await...of → consume async generator {tiêu thụ generator bất đồng bộ}
[...gen] → collect all values into array {gom mọi giá trị vào mảng}
When to reach for a generator {Khi nào dùng generator}:
- Infinite or very large sequences {chuỗi vô hạn / rất lớn}
- Lazy evaluation, avoid intermediate arrays {đánh giá lười}
- Custom iteration (trees, graphs, linked lists) {lặp tuỳ chỉnh}
- Streaming / pagination {stream / phân trang}
- State machines / step-by-step flows {máy trạng thái}
- Two-way coroutines (saga effects) {coroutine hai chiều}
Bài tập thực hành
Thử các bài này để củng cố khái niệm:
- Generator Fibonacci: yield vô hạn. rồi dùng
take()lấy 10 số đầu. - nhóm iterable thành các mảng cỡ
size, - yield cặp
[index, value]nhưenumeratecủa Python. - Bộ duyệt entries object: yield mọi lá
[path, value]của object lồng nhau - Coroutine thử lại: thử lại effect async lỗi tối đa N lần.
Generator không phải thứ kỳ lạ — một khi mô hình hàm-tạm-dừng “thông”, bạn sẽ bắt đầu thấy những vấn đề chúng giải quyết một cách thanh lịch: bất cứ đâu bạn có chuỗi theo thời gian, dữ liệu lười, hoặc luồng điều khiển từng bước.