Iterables
An array is the obvious thing you can loop over with for..of. But arrays are not special here. They just happen to follow a protocol, and any object that follows the same protocol becomes loopable too. That protocol is what “iterable” means.
Think about the objects you actually deal with: a set of tags, a list of database rows, a range of numbers, a stream of characters in a string. None of them need to be a real array. If an object represents a collection of things, for..of is the natural way to walk it — you just have to teach the object how to hand out its items one at a time.
Arrays and strings already know how. This chapter shows you the machinery underneath so you can make your own objects work the same way.
Symbol.iterator
The fastest way to understand iterables is to build one. Take an object that clearly represents a collection but is not an array — the floors an elevator will stop at:
let floorRange = {
bottom: 2,
top: 6
};
// The goal: make this work
// for (let floor of floorRange) ... floor = 2, 3, 4, 5, 6
To make floorRange work with for..of, you add one method to it, keyed by the built-in symbol Symbol.iterator. The name is a symbol on purpose — it can never collide with a regular string property you might already have, like floorRange.iterator.
Here is the contract for..of follows, step by step:
- When the loop starts, it calls
floorRange[Symbol.iterator]()exactly once. If that method is missing,for..ofthrows. The call must return an iterator — an object that has anext()method. - From then on,
for..oftalks only to that returned iterator. The original object is out of the picture. - Each time the loop needs a value, it calls
iterator.next(). - The return value of
next()must be an object shaped like{ done: Boolean, value: any }. Whendoneistrue, the loop stops. Otherwise,valueis the next item.
Here is the full implementation, with the four steps marked in the code:
let floorRange = {
bottom: 2,
top: 6
};
// 1. for..of calls this method first
floorRange[Symbol.iterator] = function() {
// ...and it returns the iterator object:
// 2. From here on, for..of works only with this object, asking it for values
return {
current: this.bottom,
last: this.top,
// 3. next() is called on every iteration by for..of
next() {
// 4. it returns a value wrapped as { done, value }
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
};
// now it works!
for (let floor of floorRange) {
alert(floor); // 2, then 3, 4, 5, 6
}
Here is that exact floorRange running live. Change bottom and top, press the button, and watch for..of pull each value out of the iterator you defined:
Notice what the design buys you: separation of concerns.
floorRangeitself has nonext()method.- A separate object — the iterator — is created by calling
floorRange[Symbol.iterator](), and that object’snext()produces the values.
The iterator is a distinct thing from the collection it walks over. That separation is the whole point, and it matters more than it first looks.
Merging the object and its iterator
Technically you can fold the two roles together and let floorRange be its own iterator. It’s less code:
let floorRange = {
bottom: 2,
top: 6,
[Symbol.iterator]() {
this.current = this.bottom;
return this;
},
next() {
if (this.current <= this.top) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
for (let floor of floorRange) {
alert(floor); // 2, then 3, 4, 5, 6
}
Now floorRange[Symbol.iterator]() returns floorRange itself. It already has a next() method, and it tracks progress in this.current. Shorter, and for simple cases that’s fine.
The cost shows up when you want two loops over the same object at once. Both share a single current, because there’s only one iterator — the object. A nested for..of over the same floorRange would stomp on the outer loop’s position.
Parallel loops over one object are rare, so the merged form is a reasonable default. Just know the tradeoff you’re making.
Strings are iterable
Arrays and strings are the two built-in iterables you’ll reach for constantly.
For a string, for..of walks over its characters:
for (let char of "code") {
// runs 4 times, once per character
alert(char); // c, then o, then d, then e
}
The important detail: it does this by character, not by UTF-16 code unit. That means surrogate pairs — characters that need two code units to encode, like many emoji and rarer scripts — come through as a single item:
let str = '🎯😎';
for (let char of str) {
alert(char); // 🎯, and then 😎
}
Compare that with indexing by str[0], which would hand you back half of a surrogate pair — a broken fragment. The string iterator knows the encoding and does the right thing.
Calling an iterator by hand
for..of is the polite front door, but you can knock on the iterator directly. This does exactly what a for..of over the string would do, only spelled out:
let str = "Hello";
// same result as:
// for (let char of str) alert(char);
let iterator = str[Symbol.iterator]();
while (true) {
let result = iterator.next();
if (result.done) break;
alert(result.value); // outputs characters one by one
}
Pulling values by hand makes the pause obvious. Press “Call next()” once per character — the iterator remembers exactly where it left off between clicks, and hands back { done: true } when the string runs out:
You rarely need this. When you do, it’s because you want control that for..of doesn’t give you — chiefly, the ability to pause. You can pull a few values, go do something else, then come back and resume from where the iterator left off. for..of runs start to finish in one go; a manual iterator lets you interleave the iteration with other work.
Iterables and array-likes
Two terms sound alike and get mixed up constantly. Pin down the difference and a lot of confusing behavior stops being confusing.
- Iterable — an object with a
Symbol.iteratormethod. It can be looped withfor..of. - Array-like — an object with numeric indexes and a
lengthproperty. It looks shaped like an array.
These are independent properties. An object can be one, the other, both, or neither.
The floorRange from earlier is iterable but not array-like: it has no indexed properties and no length.
Here’s the reverse — array-like but not iterable:
let arrayLike = { // has indexes and length => array-like
0: "red",
1: "green",
length: 2
};
// Error: arrayLike is not iterable (no Symbol.iterator)
for (let item of arrayLike) {}
Both iterables and array-likes are usually not real arrays. They don’t have push, pop, map, or any of the array toolbox. That’s the practical annoyance: you’ve got something list-shaped and you’d love to run array methods on it. You need a way to turn it into a genuine array.
Array.from
Array.from is the universal converter. Hand it an iterable or an array-like, and it builds a real Array you can then treat like any other array.
Start with the array-like case:
let arrayLike = {
0: "red",
1: "green",
length: 2
};
let arr = Array.from(arrayLike); // (*)
alert(arr.pop()); // green (a real array method works)
At line (*), Array.from inspects the object, decides whether it’s iterable or array-like, then copies every item into a new array.
Try it below. Type some comma-separated words — they get stored in a bare array-like object (numeric keys plus length, no array methods). Calling .map on it directly fails; Array.from first turns it into a genuine array, and then .map works:
It works on iterables the same way:
// floorRange is the iterable object from earlier
let arr = Array.from(floorRange);
alert(arr); // 2,3,4,5,6 (array-to-string conversion)
The mapping argument
The full signature takes two optional extras:
Array.from(obj[, mapFn, thisArg])
mapFn runs on each element before it lands in the new array, and thisArg sets what this means inside mapFn. It’s map fused into the conversion, so you don’t build an intermediate array just to transform it.
// floorRange is the iterable from earlier
// square each number as it's copied
let arr = Array.from(floorRange, num => num * num);
alert(arr); // 4,9,16,25,36
A surrogate-safe way to split strings
Because Array.from uses the iterable behavior of a string, it splits by character — surrogate pairs stay intact:
let str = '🎯😎';
// split str into an array of characters
let chars = Array.from(str);
alert(chars[0]); // 🎯
alert(chars[1]); // 😎
alert(chars.length); // 2
That’s a real advantage over str.split(''), which cuts on UTF-16 code units and would break those two characters into four broken halves.
See both side by side. Type any string — mixing emoji or rare scripts with plain letters is the interesting case — and compare how each method chops it:
Under the hood, Array.from(str) is doing the same loop you’d write by hand:
let str = '🎯😎';
let chars = []; // Array.from does essentially this internally
for (let char of str) {
chars.push(char);
}
alert(chars);
…just in one call.
You can build on this to write a slice that respects characters, which the native String.prototype.slice does not:
function slice(str, start, end) {
return Array.from(str).slice(start, end).join('');
}
let str = '🎯😎🚀';
alert( slice(str, 1, 3) ); // 😎🚀
// the native method cuts by code unit
alert( str.slice(1, 3) ); // garbage (halves of two different surrogate pairs)
Summary
An object that works in for..of is iterable.
- Technically, an iterable implements a method named
Symbol.iterator.- Calling
obj[Symbol.iterator]()returns an iterator — the object that drives the iteration. - An iterator has a
next()method returning{ done: Boolean, value: any }. Whendoneistrue, iteration is over; otherwisevalueis the next item.
- Calling
for..ofcallsSymbol.iteratorautomatically, but you can call it yourself for finer control, including pausing and resuming.- Built-ins like strings and arrays implement
Symbol.iterator. - The string iterator is surrogate-pair-aware, so it walks by character.
An array-like object has numeric indexes and a length. It may carry other properties and methods too, but it lacks the built-in array methods.
Iterable and array-like are separate traits — an object can be either, both, or neither. Look inside the language spec and you’ll find most built-in methods are written to accept iterables or array-likes rather than “real” arrays, because that’s the more general contract.
Array.from(obj[, mapFn, thisArg]) builds a genuine Array from any iterable or array-like obj, after which the full array toolbox is available. The optional mapFn and thisArg let you transform each item during the copy.