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.
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 15500Key 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 awaitconsume 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.
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
})();