JavaScript · Syntax · Advanced
Iterators and generators
Build custom iterable objects using `Symbol.iterator`, write generator functions with `function*` and `yield`, and use `yield*` to delegate.
Quick topic start and explanations before exercises (exercises below):
yield*, sending values with .next(), async generators
#Iterator/Iterable/Generator table, consumers, infinite sequences, early termination
#Exercises:
Implement the iterator protocol
#Create a `Counter` object that implements the iterator protocol manually. It should count from `start` to `end` (inclusive). Add a `[Symbol.iterator]()` method that returns an object with a `next()` method. Use it in a `for...of` loop.
function makeCounter(start, end) {
return {
[Symbol.iterator]() {
// return iterator object with next()
}
};
}
for (const n of makeCounter(1, 5)) {
console.log(n); // 1 2 3 4 5
}
Solution
function makeCounter(start, end) {
return {
[Symbol.iterator]() {
let current = start;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
}
};
}
};
}
for (const n of makeCounter(1, 5)) {
console.log(n); // 1 2 3 4 5
}
Generator function with function*
#Write a generator function `range(start, end, step = 1)` that yields numbers from `start` up to (but not including) `end`, incrementing by `step`. Use it to print numbers 0, 2, 4, 6, 8.
function* range(start, end, step = 1) {
// yield numbers here
}
for (const n of range(0, 10, 2)) {
console.log(n); // 0 2 4 6 8
}
Solution
function* range(start, end, step = 1) {
for (let i = start; i < end; i += step) {
yield i;
}
}
for (const n of range(0, 10, 2)) {
console.log(n); // 0 2 4 6 8
}
Infinite generator: Fibonacci
#Write an infinite generator `fibonacci()` that yields the Fibonacci sequence indefinitely (0, 1, 1, 2, 3, 5, 8, ...). Use it to print the first 8 Fibonacci numbers by calling `.next()` or breaking out of a `for...of` loop with a counter.
function* fibonacci() {
// infinite sequence
}
const gen = fibonacci();
for (let i = 0; i < 8; i++) {
console.log(gen.next().value);
}
// 0 1 1 2 3 5 8 13
Solution
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const gen = fibonacci();
for (let i = 0; i < 8; i++) {
console.log(gen.next().value);
}
// 0 1 1 2 3 5 8 13
yield* — delegate to another generator
#Write two generators: `odds(n)` yields odd numbers 1, 3, 5, ..., up to n; `evens(n)` yields even numbers 2, 4, 6, ..., up to n. Write a third generator `oddsAndEvens(n)` that uses `yield*` to first delegate to `odds(n)` then to `evens(n)`.
function* odds(n) {
for (let i = 1; i <= n; i += 2) yield i;
}
function* evens(n) {
for (let i = 2; i <= n; i += 2) yield i;
}
function* oddsAndEvens(n) {
// use yield* here
}
console.log([...oddsAndEvens(6)]);
// [1, 3, 5, 2, 4, 6]
Solution
function* odds(n) {
for (let i = 1; i <= n; i += 2) yield i;
}
function* evens(n) {
for (let i = 2; i <= n; i += 2) yield i;
}
function* oddsAndEvens(n) {
yield* odds(n);
yield* evens(n);
}
console.log([...oddsAndEvens(6)]);
// [1, 3, 5, 2, 4, 6]
Make a class iterable
#Create a class `NumberRange` with `constructor(start, end)`. Add `[Symbol.iterator]()` as a generator method so instances work with `for...of`, spread, and destructuring.
class NumberRange {
constructor(start, end) {
this.start = start;
this.end = end;
}
*[Symbol.iterator]() {
// yield values here
}
}
const r = new NumberRange(1, 5);
console.log([...r]); // [1, 2, 3, 4, 5]
const [a, b] = r;
console.log(a, b); // 1 2
for (const n of r) console.log(n); // 1 2 3 4 5
Solution
class NumberRange {
constructor(start, end) {
this.start = start;
this.end = end;
}
*[Symbol.iterator]() {
for (let i = this.start; i <= this.end; i++) {
yield i;
}
}
}
const r = new NumberRange(1, 5);
console.log([...r]); // [1, 2, 3, 4, 5]
const [a, b] = r;
console.log(a, b); // 1 2
for (const n of r) console.log(n);
Generator pipeline
#Build a pipeline of three generators: `naturals()` — infinite: 1, 2, 3, ...; `take(gen, n)` — takes first n values from a generator; `squares(gen)` — maps each value to its square. Print the first 5 perfect squares using `take(squares(naturals()), 5)`.
function* naturals() {
let n = 1;
while (true) yield n++;
}
function* take(gen, n) {
// yield first n values from gen
}
function* squares(gen) {
// yield x*x for each x in gen
}
console.log([...take(squares(naturals()), 5)]);
// [1, 4, 9, 16, 25]
Solution
function* naturals() {
let n = 1;
while (true) yield n++;
}
function* take(gen, n) {
let count = 0;
for (const val of gen) {
if (count++ >= n) break;
yield val;
}
}
function* squares(gen) {
for (const x of gen) yield x * x;
}
console.log([...take(squares(naturals()), 5)]);
// [1, 4, 9, 16, 25]
Generator return value
#Write a generator `countdown(n)` that yields n, n-1, ..., 1, and then returns `'Done!'`. Show that the return value appears in the final `.next()` call as `{ value: 'Done!', done: true }` but is NOT yielded by `for...of`.
function* countdown(n) {
while (n > 0) yield n--;
return 'Done!';
}
const gen = countdown(3);
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 'Done!', done: true }
// for...of ignores the return value:
for (const n of countdown(3)) {
process.stdout.write(n + ' '); // 3 2 1
}
Solution
function* countdown(n) {
while (n > 0) yield n--;
return 'Done!';
}
const gen = countdown(3);
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 'Done!', done: true }
for (const n of countdown(3)) {
process.stdout.write(n + ' '); // 3 2 1
}
Spread and destructuring with custom iterables
#Create a `LinkedList` class where each node has `value` and `next`. Add `[Symbol.iterator]()` so the list can be spread into an array and destructured. Build a list 1 → 2 → 3 and verify `[...list]` gives `[1, 2, 3]`.
class LinkedList {
constructor() { this.head = null; }
push(value) {
this.head = { value, next: this.head };
return this;
}
*[Symbol.iterator]() {
// traverse from head
}
}
const list = new LinkedList();
list.push(3).push(2).push(1);
console.log([...list]); // [1, 2, 3]
const [first, second] = list;
console.log(first, second); // 1 2
Solution
class LinkedList {
constructor() { this.head = null; }
push(value) {
this.head = { value, next: this.head };
return this;
}
*[Symbol.iterator]() {
let node = this.head;
while (node) {
yield node.value;
node = node.next;
}
}
}
const list = new LinkedList();
list.push(3).push(2).push(1);
console.log([...list]); // [1, 2, 3]
const [first, second] = list;
console.log(first, second); // 1 2
Async generator with for await...of
#Write an async generator `asyncRange(start, end, delay)` that yields numbers from `start` to `end`, waiting `delay` ms between each. Consume it with `for await...of` inside an async `main()` function.
async function* asyncRange(start, end, delay) {
for (let i = start; i <= end; i++) {
await new Promise(r => setTimeout(r, delay));
yield i;
}
}
async function main() {
// use for await...of here
}
main();
Solution
async function* asyncRange(start, end, delay) {
for (let i = start; i <= end; i++) {
await new Promise(r => setTimeout(r, delay));
yield i;
}
}
async function main() {
for await (const n of asyncRange(1, 4, 50)) {
console.log(n); // 1 2 3 4 (50ms apart)
}
}
main();
Add Symbol.iterator to a plain object
#Given `const deck = { suits: ['♠','♥','♦','♣'], values: ['A','2','3'] }`, add a `[Symbol.iterator]` generator method that yields all card combinations (`'A♠'`, `'A♥'`, ..., `'3♣'`). Verify with `[...deck]`.
const deck = {
suits: ['spade', 'heart', 'diamond', 'club'],
values: ['A', '2', '3'],
*[Symbol.iterator]() {
// yield all value+suit combos
}
};
console.log([...deck].length); // 12
console.log([...deck][0]); // 'A spade'
console.log([...deck][11]); // '3 club'
Solution
const deck = {
suits: ['spade', 'heart', 'diamond', 'club'],
values: ['A', '2', '3'],
*[Symbol.iterator]() {
for (const value of this.values) {
for (const suit of this.suits) {
yield `${value} ${suit}`;
}
}
}
};
console.log([...deck].length); // 12
console.log([...deck][0]); // 'A spade'
console.log([...deck][11]); // '3 club'