AdvancedJavaScript · Lesson 2 of 9

Iterators, Generators & Symbols

Make your own objects work with for...of and spread, and produce values lazily with generators.

Anything with a [Symbol.iterator] method is iterable: arrays, strings, Maps, Sets, NodeLists — and your own objects. Iterables work with for...of, spread ([...x]), destructuring and Array.from.

Generator functions (function*) make iterators easy: each yield hands out one value and pauses until the next is requested. They are lazy, so they can describe huge or infinite sequences that are computed only as far as needed.

Symbols are unique keys that never clash with ordinary property names. Built-in "well-known" symbols (Symbol.iterator, Symbol.toPrimitive, Symbol.asyncIterator) let your objects plug into language features. Async generators (async function*) combine with for await...of for paginated APIs and streams.

iterators.jsJavaScript
class Timetable {
  #periods = [];

  add(subject, start) {
    this.#periods.push({ subject, start });
    return this;
  }

  *[Symbol.iterator]() {                         // makes Timetable iterable
    yield* this.#periods.toSorted((a, b) => a.start.localeCompare(b.start));
  }
}

const monday = new Timetable().add("Biology", "10:00").add("Maths", "08:00").add("English", "11:40");
for (const p of monday) console.log(p.start, p.subject);
console.log([...monday].map((p) => p.subject));

function* ids(prefix) {                          // infinite, but lazy
  let n = 1;
  while (true) yield `${prefix}-${String(n++).padStart(4, "0")}`;
}

function* take(iterable, count) {
  for (const value of iterable) {
    if (count-- <= 0) return;
    yield value;
  }
}

console.log([...take(ids("S2026"), 3)]);       // [ 'S2026-0001', 'S2026-0002', 'S2026-0003' ]

async function* pages(totalPages) {             // e.g. a paginated API
  for (let page = 1; page <= totalPages; page++) {
    await new Promise((r) => setTimeout(r, 10));
    yield { page, items: [`item ${page}a`, `item ${page}b`] };
  }
}

(async () => {
  for await (const { page, items } of pages(3)) console.log(page, items);
})();

const money = {
  amount: 15000,
  [Symbol.toPrimitive](hint) {
    return hint === "number" ? this.amount : `TSh ${this.amount.toLocaleString("en-US")}`;
  },
};
console.log(`${money}`, +money + 500);       // TSh 15,000 15500
Runs in your browser · JavaScript

Key points

  • Implement [Symbol.iterator] to make your objects work with for...of and spread.
  • Generators produce values lazily — great for sequences, pipelines and pagination.
  • Async generators + for await consume paged or streamed data cleanly.

Exercise

Write a generator chunked(array, size) and a generator range(start, end, step). Then write an async generator that "fetches" pages from an array of 25 records, 10 per page, and collect all records with for await.

Show solution

Try the exercise yourself first — then compare your approach with this one.

chunked and range are ordinary generators. fetchPages is an async generator that "fetches" one page at a time and stops when a page comes back short; for await collects every record without knowing how many pages there are.

generators-solution.jsJavaScript
function* chunked(array, size) {
  for (let i = 0; i < array.length; i += size) yield array.slice(i, i + size);
}

function* range(start, end, step = 1) {
  for (let n = start; step > 0 ? n < end : n > end; n += step) yield n;
}

console.log([...chunked([1, 2, 3, 4, 5], 2)]);     // [ [ 1, 2 ], [ 3, 4 ], [ 5 ] ]
console.log([...range(0, 10, 3)], [...range(5, 0, -2)]);   // [ 0, 3, 6, 9 ] [ 5, 3, 1 ]

const records = Array.from({ length: 25 }, (_, i) => `record ${i + 1}`);

async function fakeApi(page, perPage) {
  await new Promise((r) => setTimeout(r, 10));
  return records.slice((page - 1) * perPage, page * perPage);
}

async function* fetchPages(perPage = 10) {
  for (let page = 1; ; page++) {
    const items = await fakeApi(page, perPage);
    yield { page, items };
    if (items.length < perPage) return;           // last page
  }
}

(async () => {
  const all = [];
  for await (const { page, items } of fetchPages()) {
    console.log(`page ${page}: ${items.length} records`);
    all.push(...items);
  }
  console.log(all.length, all.at(-1));            // 25 record 25
})();
Runs in your browser · JavaScript

Check your understanding

  1. What makes an object work with for...of and spread [...x]?

  2. What does yield do in a generator function?

  3. Why can a generator describe an infinite sequence safely?

  4. Which loop consumes an async generator?

Ask AI