JAVASCRIPT / DESTRUCTURING, ITERABLES, AND GENERATORS
The iterable protocol and for...of
Drive an iterator by hand with Symbol.iterator and next(), and predict exactly what for...of yields for arrays, strings, Maps, Sets and plain objects.
What you will learn
- Call arr[Symbol.iterator]().next() by hand and read the {value, done} results
- Predict which values for...of accepts and why plain objects throw a TypeError
- Pick for...of for values and for...in for keys without mixing the two up
- Destructure a Map's [key, value] pairs right in the for...of loop head
Understanding The iterable protocol and for...of
An object is iterable when it has a method stored under the key Symbol.iterator, and that method returns an iterator: an object with a next() method that hands back a {value, done} result on each call. for...of is a compact way of running that handshake, calling Symbol.iterator once to get a fresh iterator, then calling next() over and over, binding value to the loop variable each time, and stopping the first time done is true. The result that carries done: true is discarded, which is why the loop body never runs one extra time with a trailing undefined.
Nothing in that contract mentions indexes or length, so for...of has no idea whether it is walking an array, a string, a Map, a Set, arguments or a DOM NodeList; it just asks for an iterator and pulls. That is why one loop shape covers all of them, and also why for (const v of {a: 1}) throws a TypeError: a plain object literal has no Symbol.iterator method, so there is nothing to pull from. for...of is not 'for each property', it is 'for each value this object agrees to hand out'.
The same protocol backs spread, array destructuring, Array.from, new Set(...) and new Map(...), so anything you can loop with for...of you can also write as [...value]. Because values arrive one next() call at a time, the loop holds a single item instead of a whole copy, and it can quit early: break, return or throw inside the body calls the iterator's optional return() method so the source can release whatever it was holding. That cooperative shutdown is exactly why break works inside for...of and does nothing inside forEach.
const langs = ['js', 'py'];
// Spell out what for...of does under the hood.
const it = langs[Symbol.iterator]();
let step = it.next();
while (!step.done) {
console.log('manual:', step.value, '| done:', step.done);
step = it.next();
}
console.log('manual:', step.value, '| done:', step.done);
// The same walk, delegated to for...of.
for (const lang of langs) {
console.log('for...of:', lang);
}
console.log(typeof langs[Symbol.iterator]);for...of never inspects indexes or properties; it asks the value for an iterator through Symbol.iterator and calls next() until done is true.
Worked examples
Keys versus values
Shows that for...in enumerates property keys while for...of consumes the iterator.
const arr = ['a', 'b'];
arr.note = 'extra';
for (const k in arr) {
console.log('in ->', k, typeof k);
}
for (const v of arr) {
console.log('of ->', v);
}Example explained
Line 1arr.note = 'extra' adds an ordinary enumerable property; arr.length stays 2.
Line 2for...in reports every enumerable string key, so 'note' shows up and the keys are strings, not numbers.
Line 3for...of ignores keys completely and pulls values from arr[Symbol.iterator](), so note is never visited.
Maps and Sets through the same door
Demonstrates pair destructuring in the loop head and where a Map's iterator comes from.
const scores = new Map([['ana', 3], ['bo', 5]]);
for (const [name, n] of scores) {
console.log(name, 'scored', n);
}
console.log(scores[Symbol.iterator] === scores.entries);
for (const n of new Set([1, 1, 2])) {
console.log('unique', n);
}Example explained
Line 1A Map's iterator yields two-element [key, value] arrays, so the loop head can destructure each one.
Line 2Map.prototype[Symbol.iterator] is the same function object as .entries, which is why the pairs arrive in insertion order.
Line 3A Set's iterator yields each value once, and the duplicate 1 was already dropped when the Set was built.
Making a plain object loopable
Shows the runtime failure on a non-iterable object and the standard conversion.
const config = { host: 'localhost', port: 8080 };
try {
for (const v of config) console.log(v);
} catch (err) {
console.log(err.name, '-', typeof config[Symbol.iterator]);
}
for (const [key, value] of Object.entries(config)) {
console.log(key, '=', value);
}Example explained
Line 1The for...of line parses fine and only fails when it runs, so the error is a TypeError, not a syntax error.
Line 2typeof config[Symbol.iterator] is 'undefined', which is precisely the lookup that failed.
Line 3Object.entries returns a real array of [key, value] pairs, and arrays are iterable, so the second loop succeeds.
Important notes
Strings iterate by code point, not by UTF-16 unit: 'a𝄞b'.length is 4, but for...of over it yields three characters.
Array-like is not the same as iterable: {length: 2, 0: 'a', 1: 'b'} throws with for...of, while arguments and NodeList define Symbol.iterator and work; wrap the rest in Array.from.
Common mistakes
Pointing for...of at an object literal: for (const v of {a: 1}) passes parsing and then throws 'is not iterable' at runtime, crashing whatever function it sits in.
Using for...in to read array values: the loop variable is the string key, so for (const i in [10, 20]) console.log(i + 1) prints 01 and 11 instead of 1 and 2.
Treating the for...of variable as an index and writing arr[item]: item is already the element, so arr['red'] evaluates to undefined.
Try it yourself
Change, predict, then run
In the browser console, run const it = 'hi'[Symbol.iterator]() and log it.next() three times, then compare that with what for (const c of 'hi') prints. Now replace the string with {0: 'h', 1: 'i', length: 2} and explain the error you get.
Open the JavaScript workspaceCheck your understanding
A helper does for (const x of value). It works for arrays, strings, Maps and Sets, but throws for {a: 1, b: 2}. What is the most accurate reason?
- Plain objects have no length property, so the loop cannot tell how many steps to take.
- for...of only accepts instances of Array or of the built-in collection classes.
- Plain objects have no method under Symbol.iterator, so for...of has no iterator to pull values from.
- for...of reads values only, so it skips the object's keys and then reports that it found nothing.
Show answer
for...of performs one check: is there a callable method at the Symbol.iterator key. Length is irrelevant, which the Set proves, since a Set exposes size and no length yet iterates fine. Class membership is irrelevant too: Map and Set work because their prototypes define Symbol.iterator, so any object you write with that method is accepted the same way.