ข้ามไปเนื้อหาหลัก

Category: guide

JavaScript Generators และ Iterators — Lazy Sequences

function*, yield, iterators protocol และ use cases จริง: infinite sequences, async generators, pipeline

· อ่านประมาณ 4 นาที

สารบัญ

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 / streamingfor await...of อ่านง่ายกว่า recursion
State machineyield เป็น “pause point” ที่ชัดเจน
Custom iteratorแทน manual next() boilerplate