- Explain the iteration protocol (
Symbol.iterator,next(),{ value, done }) and make your own object iterable - Write generator functions with
yieldfor lazy and infinite sequences - Use symbols as unique, non-colliding keys and recognise the well-known symbols
for...of works with arrays, strings, Map and Set, but not with a plain {} object — you get TypeError: ... is not iterable. What do arrays and strings have that objects lack? A special method under the key Symbol.iterator. Once you know this protocol, you can teach any object to work with for...of, spread and destructuring — and generators make it almost effortless.
Symbols
Symbol is a primitive type added in ES2015 (JavaScript has seven primitive types in total). Every call to Symbol() creates a unique value — even if the description is the same. Symbols are used as property keys that never clash with other keys: if a library adds its own symbol key to your object, it will not touch your id or name property. Symbol keys are hidden from for...in, Object.keys and JSON.stringify. Symbol.for('key') returns a symbol shared by the whole program.
const id1 = Symbol('id');
const id2 = Symbol('id');
console.log(typeof id1, id1 === id2);
console.log(id1.toString(), id1.description);
const user = { name: 'Leyla', [id1]: 501 };
console.log(user[id1]);
console.log(Object.keys(user), JSON.stringify(user));
console.log(Symbol.for('app') === Symbol.for('app'));▸ Expected output
symbol false
Symbol(id) id
501
[ 'name' ] {"name":"Leyla"}
true| Well-known symbol | What it controls |
|---|---|
Symbol.iterator | for...of, spread, destructuring |
Symbol.asyncIterator | for await...of |
Symbol.toPrimitive | how an object turns into a number or string |
Symbol.toStringTag | the name in the result of Object.prototype.toString |
The iteration protocol
An object is iterable if it has a [Symbol.iterator]() method that returns an iterator. An iterator is an object with a next() method; each call to next() returns { value, done }. done: true says the sequence has ended. for...of, spread ..., destructuring, Array.from, new Map(...) and Promise.all all use this protocol.
const letters = ['a', 'b'];
const iterator = letters[Symbol.iterator]();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());▸ Expected output
{ value: 'a', done: false }
{ value: 'b', done: false }
{ value: undefined, done: true }for...of does exactly this: it calls next() until done: true arrives.const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
return current <= last
? { value: current++, done: false }
: { value: undefined, done: true };
},
};
},
};
for (const n of range) console.log(n);
console.log([...range], Math.max(...range));
const [first, second] = range;
console.log(first, second);▸ Expected output
1 2 3 4 5 [ 1, 2, 3, 4, 5 ] 5 1 2
Generators
Writing an iterator by hand is long work. A generator function (function*) does not run its body when called; it returns a generator object, which is both an iterator and an iterable. yield hands out a value and pauses the function; the next next() resumes it right after the yield. The code runs lazily: a value is computed only when it is asked for.
function* countdown(from) {
console.log('start');
while (from > 0) {
yield from;
from--;
}
return 'liftoff';
}
const gen = countdown(3);
console.log('generator created');
console.log(gen.next());
console.log(gen.next().value, gen.next().value);
console.log(gen.next());
console.log([...countdown(2)]);▸ Expected output
generator created
start
{ value: 3, done: false }
2 1
{ value: 'liftoff', done: true }
start
[ 2, 1 ]next(). The return value arrives only together with done: true — for...of and spread ignore it.Because values are computed on demand, a generator can be infinite: while (true) is safe here, since take pulls only as many values as needed and stops. This is also handy for reading big files line by line or data that arrives page by page.
function* ids() {
let n = 1;
while (true) yield `id-${n++}`;
}
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
function* take(iterable, count) {
if (count <= 0) return;
for (const item of iterable) {
yield item;
if (--count === 0) return;
}
}
console.log([...take(ids(), 3)]);
console.log([...take(fibonacci(), 10)].join(' '));▸ Expected output
[ 'id-1', 'id-2', 'id-3' ] 0 1 1 2 3 5 8 13 21 34
Generators in practice
class Playlist {
#songs = [];
add(...titles) {
this.#songs.push(...titles);
return this;
}
*[Symbol.iterator]() {
yield* this.#songs;
}
}
const playlist = new Playlist().add('Morning', 'Caspian Waves', 'Old Baku');
for (const [index, song] of [...playlist].entries()) {
console.log(`${index + 1}. ${song}`);
}
console.log(Array.from(playlist, (song) => song.length));▸ Expected output
1. Morning 2. Caspian Waves 3. Old Baku [ 7, 13, 8 ]
yield* delegates to another iterable — here the private array. The songs stay private, yet the class works with for...of and Array.from.When data arrives over time (pages of an API, chat messages), an async generator (async function*) and the for await...of loop come in handy: the loop waits for each next piece.
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function* loadPages(total) {
for (let page = 1; page <= total; page++) {
await wait(100);
yield [`item ${page * 2 - 1}`, `item ${page * 2}`];
}
}
for await (const items of loadPages(3)) {
console.log(items);
}
console.log('All pages loaded');▸ Expected output
[ 'item 1', 'item 2' ] [ 'item 3', 'item 4' ] [ 'item 5', 'item 6' ] All pages loaded
Write the generator chunk(array, size): it splits the array into pieces of size items and yields each piece (the last piece may be shorter).
function* chunk(array, size) {
// yield slices of the array with `size` items each
}
for (const part of chunk([1, 2, 3, 4, 5, 6, 7], 3)) {
console.log(part);
}
console.log([...chunk(['a', 'b', 'c', 'd'], 2)].length);▸ Expected output
[ 1, 2, 3 ] [ 4, 5, 6 ] [ 7 ] 2
Make the countdown object iterable without a generator: its [Symbol.iterator]() method returns an object with a next() method that produces the numbers from start down to 1. Every loop must get a fresh iterator.
const countdown = {
start: 3,
// add [Symbol.iterator]() that returns { next() { ... } }
};
console.log([...countdown]);
for (const n of countdown) {
console.log(`T-${n}`);
}▸ Expected output
[ 3, 2, 1 ] T-3 T-2 T-1
Key points
- A symbol is a unique primitive; symbol keys never clash and are hidden from
Object.keysandJSON.stringify. - An iterable has a
[Symbol.iterator]()method that returns an iterator; the iterator'snext()returns{ value, done }. for...of, spread, destructuring andArray.fromwork with any iterable.function*andyieldcreate pausable, lazy — even infinite — generators.async function*andfor await...ofare for data that arrives over time.
Check yourself
10 questions. Every correct answer earns XP.
Symbol('x') === Symbol('x')?