Category: guide
JavaScript Generators และ Iterators — Lazy Sequences
function*, yield, iterators protocol และ use cases จริง: infinite sequences, async generators, pipeline
สารบัญ
Iterator Protocol
JavaScript มี protocol สำหรับ “สิ่งที่ iterate ได้” อยู่สองอัน:
Iterable — มี [Symbol.iterator]() method ที่ return Iterator
Iterator — มี next() method ที่ return { value, done }
// Manual iterator
function rangeIterator(start, end) {
let current = start;
return {
[Symbol.iterator]() { return this; },
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
},
};
}
for (const n of rangeIterator(1, 5)) {
console.log(n); // 1, 2, 3, 4, 5
}
Generator ทำสิ่งเดิมนี้ได้ด้วยโค้ดน้อยกว่ามาก
Generator Function พื้นฐาน
function* range(start, end) {
for (let i = start; i <= end; i++) {
yield i; // หยุดที่นี่ ส่งค่า แล้วรอ next()
}
}
const gen = range(1, 5);
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
// ...
console.log(gen.next()); // { value: 5, done: false }
console.log(gen.next()); // { value: undefined, done: true }
// หรือใช้ for...of
for (const n of range(1, 5)) {
console.log(n);
}
// spread
const arr = [...range(1, 5)]; // [1, 2, 3, 4, 5]
Infinite Sequences
Generator ไม่ต้องรู้ size ล่วงหน้า — สร้าง sequence ไม่จบได้:
function* naturals() {
let n = 1;
while (true) {
yield n++;
}
}
function* take(iterable, n) {
let count = 0;
for (const item of iterable) {
if (count >= n) return;
yield item;
count++;
}
}
const first10 = [...take(naturals(), 10)];
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// Fibonacci
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const fibs = [...take(fibonacci(), 8)];
// [0, 1, 1, 2, 3, 5, 8, 13]
Two-way Communication
yield สามารถรับค่ากลับจาก next(value) ได้:
function* calculator() {
let result = 0;
while (true) {
const input = yield result; // ส่ง result ออก แล้วรับ input กลับมา
if (input === null) return result;
result += input;
}
}
const calc = calculator();
calc.next(); // เริ่ม generator (yield ครั้งแรก) → { value: 0, done: false }
calc.next(10); // ส่ง 10 เข้าไป → { value: 10, done: false }
calc.next(5); // ส่ง 5 เข้าไป → { value: 15, done: false }
calc.next(null); // ส่ง null → จบ → { value: 15, done: true }
yield* — Delegate ไปยัง Iterable อื่น
function* flatten(iterable) {
for (const item of iterable) {
if (Array.isArray(item)) {
yield* flatten(item); // delegate recursively
} else {
yield item;
}
}
}
const nested = [1, [2, [3, 4]], [5, 6]];
console.log([...flatten(nested)]); // [1, 2, 3, 4, 5, 6]
Generator Pipeline
ใช้ generators เชื่อมต่อเป็น lazy pipeline — ประมวลผลทีละ item ไม่ต้องสร้าง array ใหม่:
function* map(iterable, fn) {
for (const item of iterable) {
yield fn(item);
}
}
function* filter(iterable, predicate) {
for (const item of iterable) {
if (predicate(item)) yield item;
}
}
function* take(iterable, n) {
let count = 0;
for (const item of iterable) {
if (count++ >= n) return;
yield item;
}
}
// Pipeline: naturals → map (x²) → filter (เลขคู่) → take (5)
const result = [
...take(
filter(
map(naturals(), (x) => x * x),
(x) => x % 2 === 0
),
5
)
];
// [4, 16, 36, 64, 100] — คำนวณ lazy ไม่ต้องสร้าง intermediate array ยักษ์
Async Generator
async function* fetchPages(baseUrl, startPage = 1) {
let page = startPage;
while (true) {
const res = await fetch(`${baseUrl}?page=${page}`);
if (!res.ok) return;
const data = await res.json();
if (data.items.length === 0) return;
yield data.items;
page++;
}
}
async function processAll() {
for await (const items of fetchPages('/api/products')) {
for (const item of items) {
console.log(item.name);
}
}
}
for await...of ทำงานกับ async iterables โดย await แต่ละ next() call
Use Case: Cancellable Async Work
async function* pollApi(url, intervalMs) {
while (true) {
const data = await fetch(url).then(r => r.json());
yield data;
await new Promise(resolve => setTimeout(resolve, intervalMs));
}
}
// ใช้งาน — หยุดได้โดย break หรือ return จาก for await
async function watchStatus(jobId) {
for await (const status of pollApi(`/api/jobs/${jobId}`, 2000)) {
console.log('Status:', status.state);
if (status.state === 'done' || status.state === 'error') {
break; // generator ถูก garbage collected
}
}
}
Use Case: State Machine
function* trafficLight() {
while (true) {
yield 'green';
yield 'yellow';
yield 'red';
}
}
const light = trafficLight();
const nextLight = () => light.next().value;
console.log(nextLight()); // 'green'
console.log(nextLight()); // 'yellow'
console.log(nextLight()); // 'red'
console.log(nextLight()); // 'green' (วน)
Use Case: ID Generator
function* idGenerator(prefix = '') {
let id = 1;
while (true) {
yield `${prefix}${id++}`;
}
}
const userId = idGenerator('user-');
const productId = idGenerator('prod-');
console.log(userId.next().value); // 'user-1'
console.log(userId.next().value); // 'user-2'
console.log(productId.next().value); // 'prod-1'
สรุปเมื่อไหรควรใช้
| Use Case | เหตุผล |
|---|---|
| Infinite / large sequences | ไม่ต้องสร้างทั้งหมดใน memory |
| Lazy pipeline | ประมวลผลทีละ step, composable |
| Async polling / streaming | for await...of อ่านง่ายกว่า recursion |
| State machine | yield เป็น “pause point” ที่ชัดเจน |
| Custom iterator | แทน manual next() boilerplate |