JavaScript Generators và Iterators: Hướng Dẫn Toàn Diện Từ Cơ Bản Đến Nâng Cao
Làm chủ Iterator protocol và Generator trong JavaScript: Symbol.iterator, for...of hoạt động ra sao, hàm generator, yield, yield*, truyền giá trị hai chiều, lazy evaluation, async generators, ứng dụng thực tế, hiệu năng và những cạm bẫy cần tránh.

Giới thiệu
Trong JavaScript, hầu hết chúng ta lớn lên cùng vòng lặp for và các phương thức mảng như map, filter, forEach. Nhưng có một tầng trừu tượng sâu hơn nằm bên dưới tất cả những thứ đó, chi phối cách JavaScript duyệt dữ liệu: giao thức Iterator (Iterator protocol). Và trên nền tảng đó, ES6 mang đến một công cụ mạnh mẽ nhưng bị nhiều lập trình viên bỏ qua: hàm generator (generator function).
Bạn đã dùng generator rất nhiều lần mà có thể không biết: mỗi khi viết for...of, spread một mảng [...arr], destructuring const [a, b] = arr, hay for await...of để đọc một luồng dữ liệu — bạn đang sử dụng giao thức Iterator. Còn generator là cỗ máy đằng sau tính năng async/await ngay từ những ngày đầu, và là chìa khóa để viết code xử lý dữ liệu lười biếng (lazy), tiết kiệm bộ nhớ và có thể biểu diễn cả chuỗi vô hạn.
Bài viết này sẽ đi từ con số không: Iterator protocol là gì, for...of thực chất hoạt động ra sao, cách tự tay viết một iterable, rồi tiến dần đến hàm generator, yield, yield*, truyền giá trị hai chiều vào generator, async generator và những ứng dụng thực tế trong công việc hằng ngày. Cuối bài là phần cheat sheet, cạm bẫy và FAQ.
Iterator protocol là gì?
Ở tầng thấp nhất, JavaScript định nghĩa một hợp đồng (contract) để bất kỳ đối tượng nào cũng có thể “biết cách lặp”. Hợp đồng đó gọi là Iterator protocol:
Một iterator là một đối tượng có phương thức
next(). Mỗi lần gọinext(), nó trả về một object dạng{ value, done }.
value: giá trị của bước lặp hiện tại.done:falsenếu còn phần tử,truekhi đã duyệt hết.
Khi done là true, value thường là undefined (trừ khi generator return một giá trị). Hãy tự tay viết một iterator để thấy rõ bản chất:
function makeCounterIterator(max) {
let count = 1;
return {
next() {
if (count <= max) {
return { value: count++, done: false };
}
return { value: undefined, done: true };
},
};
}
const it = makeCounterIterator(3);
console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }
console.log(it.next()); // { value: 3, done: false }
console.log(it.next()); // { value: undefined, done: true }
console.log(it.next()); // { value: undefined, done: true }
Iterator này giữ trạng thái bên trong (count) và tiến dần mỗi lần next(). Đây chính là ý tưởng cốt lõi: một đối tượng “có nhớ” vị trí hiện tại của nó.
Iterable protocol và Symbol.iterator
Bản thân iterator mới chỉ là một nửa câu chuyện. Để for...of, spread hay destructuring hoạt động, đối tượng cần là iterable — tức là nó phải cung cấp phương thức đặc biệt [Symbol.iterator], và phương thức này trả về một iterator.
const iterable = {
data: ['a', 'b', 'c'],
[Symbol.iterator]() {
let index = 0;
const data = this.data;
return {
next() {
if (index < data.length) {
return { value: data[index++], done: false };
}
return { value: undefined, done: true };
},
};
},
};
for (const item of iterable) {
console.log(item); // 'a', 'b', 'c'
}
console.log([...iterable]); // ['a', 'b', 'c']
const [first, second] = iterable; // 'a', 'b'
Symbol.iterator là một trong những “well-known symbol” của JavaScript. Nó là tên thuộc tính ẩn mà các cơ chế ngôn ngữ tìm tới để biết một đối tượng có lặp được hay không.
Nhiều kiểu dữ liệu built-in đã sẵn là iterable:
[][Symbol.iterator]; // function → Array
''[Symbol.iterator]; // function → String (lặp theo từng ký tự Unicode)
new Map()[Symbol.iterator]; // function → Map (mỗi bước là [key, value])
new Set()[Symbol.iterator]; // function → Set
Ngược lại, object thường không iterable — đó là lý do bạn không thể for...of trực tiếp một object:
const obj = { a: 1, b: 2 };
for (const x of obj) {} // ❌ TypeError: obj is not iterable
// Phải đi qua entries():
for (const [key, value] of Object.entries(obj)) {
console.log(key, value); // 'a' 1, rồi 'b' 2
}
for…of hoạt động như thế nào?
Hiểu được hai giao thức trên, bạn có thể “mổ xẻ” for...of. Cú pháp này thực chất là đường tắt của một quy trình khá máy móc:
// Viết bằng for...of
for (const item of iterable) {
console.log(item);
}
// Tương đương (mô phỏng thủ công):
const iterator = iterable[Symbol.iterator]();
let result = iterator.next();
while (!result.done) {
const item = result.value;
console.log(item);
result = iterator.next();
}
Đây là lý do một đối tượng chỉ cần có [Symbol.iterator] là lặp được với for...of, spread, destructuring, Array.from, Promise.all, new Set(), new Map(), yield*… Tất cả đều dùng chung một giao thức. Đây là sức mạnh của tính đa hình (polymorphism) ở tầng ngôn ngữ.
Ghi chú: Khi vòng lặp kết thúc sớm bằng
break,return, hoặc lỗi ném ra, JavaScript sẽ tự động gọiiterator.return()nếu tồn tại (nếu có). Đây là cơ chế để dọn dẹp tài nguyên — generator tận dụng điểm này để chạy khốifinally.
Generator function — cú pháp và cách hoạt động
Tự viết iterator bằng tay như ví dụ đầu bài rất dài dòng. Đó chính là vấn đề mà generator giải quyết. Hàm generator được khai báo bằng dấu * và dùng từ khóa yield:
function* makeCounter(max) {
let count = 1;
while (count <= max) {
yield count; // tạm dừng và trả giá trị ra ngoài
count++;
}
}
const counter = makeCounter(3);
console.log(counter.next()); // { value: 1, done: false }
console.log(counter.next()); // { value: 2, done: false }
console.log(counter.next()); // { value: 3, done: false }
console.log(counter.next()); // { value: undefined, done: true }
Bên trong, một generator object được tạo ra. Nó vừa là iterator (có next()), vừa là iterable (có [Symbol.iterator] trả về chính nó). Nghĩa là bạn có thể duyệt nó bằng for...of luôn:
for (const n of makeCounter(5)) {
console.log(n); // 1 2 3 4 5
}
console.log([...makeCounter(4)]); // [1, 2, 3, 4]
Điểm mấu chốt: generator KHÔNG chạy ngay
Đây là điều quan trọng nhất cần khắc cốt ghi tâm:
function* demo() {
console.log('Bắt đầu chạy!');
yield 1;
console.log('Tiếp tục...');
yield 2;
}
const gen = demo(); // KHÔNG in gì cả — thân hàm chưa chạy
console.log('Đã tạo generator');
gen.next(); // in 'Bắt đầu chạy!' rồi dừng ở yield 1
gen.next(); // in 'Tiếp tục...' rồi dừng ở yield 2
Khi bạn gọi demo(), JavaScript chỉ tạo generator object ở trạng thái suspended. Thân hàm không chạy một dòng nào — kể cả dòng console.log đầu tiên. Nó chỉ bắt đầu khi bạn gọi next() đầu tiên, chạy đến yield đầu tiên rồi đóng băng toàn bộ trạng thái (biến cục bộ, vị trí con trỏ lệnh) để lần next() sau tiếp tục đúng chỗ đó.
Chính khả năng “tạm dừng và tiếp tục” này là nền tảng của lazy evaluation và coroutine.
yield vs return
function* example() {
yield 'a'; // tạm dừng, done: false
yield 'b'; // tạm dừng, done: false
return 'c'; // kết thúc, done: true
yield 'd'; // không bao giờ chạy tới
}
const g = example();
console.log(g.next()); // { value: 'a', done: false }
console.log(g.next()); // { value: 'b', done: false }
console.log(g.next()); // { value: 'c', done: true } ← giá trị return
console.log(g.next()); // { value: undefined, done: true }
// Cực kỳ quan trọng: for...of BỎ QUA giá trị return!
console.log([...example()]); // ['a', 'b'] — không có 'c'
Hãy nhớ kỹ: return kết thúc generator vĩnh viễn và giá trị của nó bị for...of bỏ qua. Nhiều người mới bị nhầm ở điểm này, tưởng phần tử return sẽ xuất hiện trong vòng lặp.
yield* — ủy quyền cho iterable khác
Có những lúc bạn muốn một generator “trải” nội dung của iterable khác ra ngoài. Thay vì viết vòng lặp, dùng yield*:
function* inner() {
yield 2;
yield 3;
}
function* outer() {
yield 1;
yield* inner(); // ủy quyền: trải ra 2, 3
yield* [4, 5]; // nhận mảng
yield* 'AB'; // nhận string → 'A', 'B'
}
console.log([...outer()]); // [1, 2, 3, 4, 5, 'A', 'B']
yield* nhận bất kỳ iterable nào — mảng, chuỗi, Set, Map, hay generator khác. Đặc biệt, nó không chỉ trải giá trị mà còn chuyển tiếp next(value), throw() và return() tới generator bên trong. Nhờ đó nó mạnh hơn hẳn việc dùng for...of rồi yield từng phần tử khi bạn cần đệ quy.
Ứng dụng kinh điển là duyệt cây đệ quy:
function* walk(tree) {
yield tree.value;
for (const child of tree.children) {
yield* walk(child); // đệ quy trải toàn bộ cây con
}
}
const tree = {
value: 1,
children: [
{ value: 2, children: [] },
{ value: 3, children: [{ value: 4, children: [] }] },
],
};
console.log([...walk(tree)]); // [1, 2, 3, 4]
Với yield*, việc duyệt cây sâu vừa gọn vừa thể hiện đúng bản chất đệ quy của cấu trúc.
Truyền giá trị hai chiều vào generator
Generator không chỉ “đẩy” giá trị ra ngoài. Nó còn có thể nhận giá trị từ bên ngoài gửi vào thông qua next(value):
function* dialogue() {
const name = yield 'Tên bạn là gì?';
const age = yield `Chào ${name}, bạn bao nhiêu tuổi?`;
return `${name}, ${age} tuổi.`;
}
const d = dialogue();
console.log(d.next().value); // 'Tên bạn là gì?' ← next() đầu bỏ qua tham số
console.log(d.next('Alice').value); // 'Chào Alice, bạn bao nhiêu tuổi?'
console.log(d.next(30).value); // 'Alice, 30 tuổi.'
Điểm tinh tế: giá trị truyền vào next(value) trở thành kết quả của biểu thức yield đang tạm dừng. Vì next() đầu tiên chưa có yield nào để gán, tham số của nó luôn bị bỏ qua.
Generator còn có hai phương thức điều khiển khác:
function* task() {
try {
yield 1;
yield 2;
} finally {
console.log('Dọn dẹp tài nguyên!');
}
}
const t = task();
t.next(); // { value: 1, done: false }
t.return('xong'); // in 'Dọn dẹp tài nguyên!' rồi { value: 'xong', done: true }
generator.return(value): kết thúc sớm generator, chạy khốifinallynếu có.generator.throw(error): ném lỗi vào đúng điểmyieldđang dừng, để generator xử lý bằngtry...catch.
Hai phương thức này biến generator thành một coroutine hai chiều thực thụ — nền tảng mà các thư viện như redux-saga từng dùng để mô tả luồng side-effect.
Generator và chuỗi vô hạn (lazy evaluation)
Vì generator chỉ sinh giá trị khi được yêu cầu, nó biểu diễn được cả chuỗi vô hạn mà không tràn bộ nhớ:
function* infiniteIds() {
let id = 1;
while (true) {
yield id++;
}
}
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
// Helper "take" để lấy n phần tử đầu — BẮT BUỘC khi dùng chuỗi vô hạn
function* take(n, iterable) {
if (n <= 0) return;
let count = 0;
for (const item of iterable) {
yield item;
if (++count >= n) return;
}
}
console.log([...take(5, infiniteIds())]); // [1, 2, 3, 4, 5]
console.log([...take(8, fibonacci())]); // [0, 1, 1, 2, 3, 5, 8, 13]
Hãy tưởng tượng bạn cần sinh 1 triệu ID duy nhất. Với mảng, bạn phải cấp phát 1 triệu phần tử cùng lúc. Với generator, bạn chỉ giữ một biến id trong bộ nhớ và sinh dần từng giá trị khi cần. Chênh lệch bộ nhớ là khổng lồ.
Đây là lý do generator được dùng nhiều trong data pipeline: bạn có thể ghép nhiều generator lại thành một dây chuyền xử lý, mỗi phần tử chảy qua toàn bộ pipeline mà không cần dựng mảng trung gian:
function* naturals() {
let n = 1;
while (true) yield n++;
}
function* mapGen(iterable, fn) {
for (const x of iterable) yield fn(x);
}
function* filterGen(iterable, pred) {
for (const x of iterable) if (pred(x)) yield x;
}
const pipeline = take(
5,
filterGen(
mapGen(naturals(), (x) => x * x), // bình phương
(x) => x % 2 === 0 // chỉ lấy số chẵn
)
);
console.log([...pipeline]); // [4, 16, 36, 64, 100]
Async generator và for await…of
Generator thường xử lý giá trị đồng bộ. Nhưng khi dữ liệu đến từ mạng, file, hay stream, ta cần async generator — khai báo bằng async function*, dùng được cả await lẫn yield:
async function* fetchPages(baseUrl, totalPages) {
for (let page = 1; page <= totalPages; page++) {
const res = await fetch(`${baseUrl}?page=${page}`);
const data = await res.json();
// trải từng item của trang ra ngoài
for (const item of data.items) {
yield item;
}
}
}
// Duyệt bằng for await...of
for await (const item of fetchPages('/api/articles', 3)) {
console.log(item.title);
}
Iterator của async generator trả về Promise<{ value, done }>, nên for await...of sẽ await từng bước. Đây là cách viết code streaming gọn gàng nhất trong JavaScript hiện đại — minh bạch hơn callback, sạch hơn chuỗi .then() lồng nhau.
Một ứng dụng cực kỳ thực tế: đọc response stream theo chunk:
async function* readChunks(response) {
const reader = response.body.getReader();
const decoder = new TextDecoder();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
yield decoder.decode(value, { stream: true });
}
} finally {
reader.releaseLock();
}
}
const res = await fetch('/api/stream');
for await (const chunk of readChunks(res)) {
process.stdout.write(chunk); // in dần khi dữ liệu về
}
Khối finally đảm bảo reader được giải phóng kể cả khi vòng lặp break giữa chừng — một lợi ích dọn dẹp tài nguyên mà generator cung cấp miễn phí.
Ứng dụng thực tế
1. Phân trang (pagination) mượt mà
function* paginate(items, pageSize) {
for (let i = 0; i < items.length; i += pageSize) {
yield items.slice(i, i + pageSize);
}
}
const articles = Array.from({ length: 25 }, (_, i) => `Bài ${i + 1}`);
for (const page of paginate(articles, 10)) {
console.log(page); // 3 trang: 10, 10, 5 phần tử
}
2. Sinh số thứ tự / ID tăng dần
function createIdGenerator(prefix) {
let seq = 0;
return function* () {
while (true) yield `${prefix}-${++seq}`;
};
}
const nextUser = createIdGenerator('user')();
console.log(nextUser.next().value); // 'user-1'
console.log(nextUser.next().value); // 'user-2'
Kết hợp closure và generator cho ra một bộ đếm đóng gói hoàn toàn — không có biến toàn cục nào bị rò rỉ.
3. Trộn hai mảng (lazy merge)
function* zip(a, b) {
const len = Math.min(a.length, b.length);
for (let i = 0; i < len; i++) {
yield [a[i], b[i]];
}
}
console.log([...zip([1, 2, 3], ['a', 'b', 'c'])]);
// [[1,'a'], [2,'b'], [3,'c']]
4. Xử lý dữ liệu lớn không tốn RAM
Giả sử bạn có file CSV 10GB. Thay vì đọc cả file vào mảng (chắc chắn hết RAM), hãy đọc theo dòng:
async function* readLines(fileStream) {
let buffer = '';
for await (const chunk of fileStream) {
buffer += chunk;
const lines = buffer.split('\n');
buffer = lines.pop(); // giữ phần dòng dở dang lại
for (const line of lines) yield line;
}
if (buffer) yield buffer; // dòng cuối cùng
}
Mỗi dòng được xử lý rồi giải phóng, bộ nhớ luôn ở mức thấp.
Hiệu năng và khi nào không nên dùng
Generator không phải viên đạn bạc. Mỗi bước next() phải:
- Tạo một object
{ value, done }mới. - Chuyển đổi trạng thái của máy generator.
- Đối với async generator, còn tạo thêm Promise.
Do đó, khi bạn có một mảng nhỏ và cần tốc độ tối đa, vòng for cổ điển vẫn nhanh nhất:
// Nhanh nhất: vòng lặp thường
for (let i = 0; i < arr.length; i++) { /* ... */ }
// Nhanh gần bằng, code gọn hơn
for (const x of arr) { /* ... */ }
// Chậm hơn một chút do overhead yield
function* gen() { yield* arr; }
Dùng generator khi:
- Cần lazy evaluation — chỉ tính khi thực sự cần.
- Dữ liệu lớn hoặc vô hạn, không thể nạp hết vào RAM.
- Xử lý theo pipeline/stream nhiều bước.
- Cần biểu diễn trạng thái lặp phức tạp (duyệt cây, đồ thị).
Tránh generator khi:
- Dữ liệu nhỏ, cố định — mảng +
map/filterđơn giản và nhanh hơn. - Trong vòng lặp nóng cần hiệu năng cực đại (hot path).
- Bạn cần truy cập ngẫu nhiên theo chỉ số — generator chỉ duyệt tuần tự một chiều.
Những cạm bẫy thường gặp
1. Quên rằng generator là “dùng một lần”. Một generator object chỉ duyệt được một lần. Duyệt xong là cạn, không thể “tua lại”:
const g = makeCounter(3);
[...g]; // [1, 2, 3]
[...g]; // [] ← đã cạn! Phải tạo generator mới
2. Spread một chuỗi vô hạn → treo máy. Đây là lỗi chết người:
// TUYỆT ĐỐI KHÔNG làm điều này:
// const all = [...infiniteIds()]; // treo vĩnh viễn, hết RAM
console.log([...take(10, infiniteIds())]); // ✅ đúng cách
3. Tưởng return trong generator giống trong hàm thường. Giá trị return không được đưa vào for...of và cũng không xuất hiện khi spread. Nếu muốn một giá trị là phần tử cuối, hãy dùng yield, đừng dùng return.
4. Dùng next() đầu tiên với tham số. Tham số của lần next() đầu tiên luôn bị bỏ qua vì chưa có yield nào đang chờ để nhận giá trị.
5. Không dọn dẹp tài nguyên. Nếu generator mở file/kết nối mạng, hãy đặt phần dọn dẹp trong khối finally. Vòng lặp for...of khi break sẽ gọi generator.return(), kích hoạt finally cho bạn — nhưng chỉ khi bạn dùng for...of hoặc yield*, chứ không phải khi bạn gọi next() thủ công rồi bỏ dở.
6. Nhầm lẫn giữa generator object và hàm generator.
function* gen() { yield 1; }
typeof gen; // 'function' — đây là hàm
const g = gen();
typeof g; // 'object' — đây là generator object
typeof g.next; // 'function'
Cheat sheet
// ===== ITERATOR PROTOCOL =====
// iterator = { next() → { value, done } }
// iterable = { [Symbol.iterator]() → iterator }
// ===== TỰ TẠO ITERABLE =====
const iterable = {
[Symbol.iterator]() {
let i = 0;
return { next: () => (i < 3 ? { value: i++, done: false } : { value: undefined, done: true }) };
},
};
// ===== GENERATOR FUNCTION =====
function* gen() {
yield 1; // tạm dừng, trả giá trị
const x = yield 2; // nhận giá trị qua next(value)
yield* [3, 4]; // ủy quyền cho iterable khác
return 5; // kết thúc (for...of bỏ qua giá trị này)
}
// TRỄ: gọi gen() chưa chạy gì cả — chỉ chạy ở next() đầu tiên
// ===== ĐIỀU KHIỂN =====
const g = gen();
g.next(); // { value: 1, done: false }
g.next('abc'); // gửi 'abc' vào biến x
g.return('x'); // kết thúc sớm, chạy finally
g.throw(err); // ném lỗi vào điểm dừng
// ===== ASYNC GENERATOR =====
async function* agen() {
const r = await fetch('/x');
yield r.json();
}
// for await (const item of agen()) { ... }
// ===== HELPERS =====
const take = function* (n, it) { let i = 0; for (const x of it) { yield x; if (++i >= n) return; } };
const range = function* (a, b) { for (let i = a; i < b; i++) yield i; };
[...range(0, 5)]; // [0, 1, 2, 3, 4]
FAQ
Iterator và Iterable khác nhau thế nào?
Iterable là đối tượng có thể lặp — nó sở hữu phương thức [Symbol.iterator]. Iterator là đối tượng thực hiện việc lặp — nó có next() trả về { value, done }. Gọi [Symbol.iterator]() trên một iterable sẽ trả về một iterator. Ví dụ: mảng là iterable (typeof [][Symbol.iterator] === 'function') và cũng tự trả về chính nó như một iterator. Generator object vừa là iterable vừa là iterator.
Hàm generator là gì và khác hàm thường ở đâu?
Hàm generator được khai báo bằng dấu * (function*, async function*) và dùng yield. Khác biệt cốt lõi: hàm thường chạy hết một mạch rồi trả về; hàm generator có thể tạm dừng giữa chừng tại mỗi yield và tiếp tục đúng chỗ đó khi gọi next(). Gọi hàm generator không chạy thân hàm — nó chỉ tạo generator object ở trạng thái suspended.
Tại sao gọi hàm generator không thực thi ngay?
Vì generator bị “đóng băng” để dùng cho lazy evaluation. Khi gọi gen(), JavaScript tạo generator object nhưng không chạy dòng code nào — kể cả dòng đầu tiên. Thân hàm chỉ thực thi từ lần next() đầu tiên, chạy tới yield đầu rồi dừng. Nhờ vậy bạn tạo được chuỗi vô hạn mà không tràn bộ nhớ.
yield khác return như thế nào?
return kết thúc generator vĩnh viễn (done: true), yield chỉ tạm dừng (done: false) và giữ trạng thái để tiếp tục. Một generator có thể yield nhiều lần nhưng chỉ return một lần ở cuối. Lưu ý: giá trị return bị for...of bỏ qua.
yield* dùng để làm gì?
yield* là cú pháp ủy quyền lặp (delegation). Viết yield* otherIterable sẽ “trải” toàn bộ giá trị của iterable đó ra ngoài. Nó nhận mảng, Set, Map, string, hay generator khác. yield* còn chuyển tiếp next(value), throw() và return() tới generator bên trong — mạnh hơn vòng lặp for...of rồi yield. Ứng dụng kinh điển là duyệt cây đệ quy.
Async generator khác generator thường chỗ nào?
Async generator (async function*) cho phép dùng cả await lẫn yield, duyệt bằng for await...of. Iterator của nó trả về Promise<{ value, done }>, nên mỗi bước có thể chờ I/O bất đồng bộ. Dùng cho luồng dữ liệu bất đồng bộ: đọc từng trang API phân trang, đọc file theo chunk, tiêu thụ stream hoặc Server-Sent Events.
Có phải generator luôn chậm hơn mảng và vòng lặp thường?
Từng bước thì có — mỗi yield tốn chi phí tạo object và chuyển trạng thái, nên vòng for thường vẫn nhanh nhất khi cần tốc độ tối đa. Nhưng generator không sinh toàn bộ dữ liệu vào bộ nhớ cùng lúc, nên với chuỗi lớn hoặc vô hạn nó tiết kiệm bộ nhớ khổng lồ. Nguyên tắc: dùng generator khi cần lazy hoặc dữ liệu khổng lồ; dùng mảng khi dữ liệu nhỏ và cần tốc độ.
Generator có thể nhận giá trị gửi vào giữa chừng không?
Có. next(value) gửi value vào generator và giá trị đó trở thành kết quả của biểu thức yield đang tạm dừng. Ví dụ const received = yield x; — lần next(42) kế tiếp khiến received bằng 42. Lần next() đầu tiên bỏ qua tham số vì chưa có yield nào để gán. Generator cũng có return(v) để kết thúc sớm và throw(err) để ném lỗi vào điểm dừng.
Làm sao để chuyển generator thành mảng hoặc ngược lại?
Từ generator sang mảng: [...gen()], Array.from(gen()), hoặc tích lũy bằng for...of. Từ mảng sang generator: function* () { yield* array; } hoặc array.values(). Lưu ý quan trọng: nếu generator sinh chuỗi vô hạn, đừng bao giờ spread hay Array.from nó — sẽ treo máy. Hãy dùng helper take(n, iterable) để chỉ lấy n phần tử đầu.
Kết luận
Iterator và generator là tầng trừu tượng nằm dưới rất nhiều tính năng JavaScript mà ta dùng hằng ngày. Nắm vững chúng, bạn không chỉ hiểu sâu hơn cách ngôn ngữ vận hành, mà còn có trong tay một công cụ cực kỳ linh hoạt:
- Iterator và Iterable protocol — nền tảng cho
for...of, spread, destructuring và mọi cơ chế lặp. - Generator function — viết iterator phức tạp một cách ngắn gọn, với khả năng tạm dừng và tiếp tục.
- Lazy evaluation — sinh giá trị khi cần, biểu diễn được cả chuỗi vô hạn, tiết kiệm bộ nhớ.
- Async generator — cách viết code xử lý luồng dữ liệu bất đồng bộ gọn gàng nhất hiện nay.
Bạn không cần dùng generator ở mọi nơi — nhưng khi bài toán rơi vào một trong những tình huống: dữ liệu khổng lồ, luồng vô hạn, pipeline nhiều bước, hay duyệt cấu trúc dạng cây — thì generator thường là lời giải thanh lịch và hiệu quả nhất.
Hãy bắt đầu từ việc nhỏ: lần tới khi viết một hàm trả về mảng rồi lặp qua nó, tự hỏi “hàm này có thể yield từng phần tử thay vì dựng cả mảng không?”. Câu trả lời sẽ thường là có — và bạn vừa tiết kiệm được một mảng lớn trong bộ nhớ.