Understanding Iterable/Iterator Protocols
for...of iterates only over objects that follow the "Iteration protocols."
for...of Why Doesn't it Work on Objects? — Understanding via Iterable/Iterator Protocols
Summary
for...ofiterates only over objects that follow the "Iteration protocols." (MDN: for…of)Array,Set,Map,Stringare iterable by default, but plain Objects are not.- When iterating over Objects, they are usually converted using
Object.entries()/values()/keys(), andSymbol.iteratoris only implemented directly when absolutely necessary. (MDN: Iteration protocols)
Background/Problem
for...of might look like a syntax for arrays, but its actual criterion is simpler.
It's all about whether it's iterable or not.
const arr = [1, 2, 3, 4];\nfor (const value of arr) console.log(value);\n\nconst set = new Set([1, 2, 3, 4]);\nfor (const value of set) console.log(value);\n\nconst obj = { 0: 1, 1: 2, 2: 3, 3: 4 };\nfor (const value of obj) console.log(value); // TypeError: obj is not iterableExpected Result / What Changed\n- arr, set are iterated.\n- obj throws an is not iterable error (message may vary by environment).
Core Concepts
1) What is an Iterable?
Symbol.iterator Refers to an object from which an iterator can be retrieved via Symbol.iterator.
If this exists, it is treated as an "iterable value" in contexts like for...of, ...spread, and Array.from(). (MDN: Symbol.iterator)
Simply put:\n- iterable protocol: obj[Symbol.iterator]() returns an iterator\n- iterator protocol: An iterator has a next() method that returns { value, done }
2) What is the iterator result { value, done }?
next() provides the "next value" each time or signals the end if there are no more values.
value: The value retrieved this time.done: Whether the iteration has ended (ended iftrue).
Because of this protocol, you can think of for...of working internally like this.
const it = [1, 2][Symbol.iterator]();\nconsole.log(it.next()); // { value: 1, done: false }\nconsole.log(it.next()); // { value: 2, done: false }\nconsole.log(it.next()); // { value: undefined, done: true }Expected Result / What Changed\n- You get the sense that "iteration" is essentially a series of next() calls.
3) Then, why aren't Objects iterable by default?
The core reason is that "Object is a type where iteration rules can easily become ambiguous."
- Should it iterate over keys, values, or [key, value] pairs?
- By what criteria should the order of properties be guaranteed?
- Properties like the prototype chain and enumerability are also involved.
Therefore, JavaScript doesn't treat Objects as iterables by default; instead, it provides explicit conversion APIs like Object.keys/values/entries.
When the iteration method is explicit in the code, it's less confusing for the reader. (MDN: Object.entries)
Solution Approach
If you need to iterate over an Object, you can generally choose in the following order.
- [key, value] pairs needed →
Object.entries(obj) - Only values needed →
Object.values(obj) - Only keys needed →
Object.keys(obj) - Truly need the for...of obj form → Implement
Symbol.iteratordirectly.
Implementation (Code)
1) Iterating over [key, value] using Object.entries()
const obj = { 0: 1, 1: 2, 2: 3, 3: 4 };\n\nfor (const [key, value] of Object.entries(obj)) {\n console.log(key, value);\n}Expected Result / What Changed\n- Convert the Object into an "iterable array form" and use it in for...of.\n- The key comes out as a string (e.g., "0"). (MDN: Object.entries)
2) If only values are needed, use Object.values()
const obj = { 0: 1, 1: 2, 2: 3, 3: 4 };\n\nfor (const value of Object.values(obj)) {\n console.log(value);\n}Expected Result / What Changed\n- Iterating only over values makes the intent clearer. (MDN: Object.values)
Map is iterable by default; what does it yield?
Map yields [key, value] pairs by default in a for...of loop.
const map = new Map([\n ["a", 1],\n ["b", 2],\n]);\n\nfor (const [k, v] of map) {\n console.log(k, v);\n}Expected Result / What Changed\n- It highlights that Map is a "structure that can list key/value pairs in order." (MDN: Map)
4) How to quickly check if something is "iterable"
function isIterable(value) {\n return value != null && typeof value[Symbol.iterator] === "function";\n}\n\nconsole.log(isIterable([1, 2, 3])); // true\nconsole.log(isIterable(new Set([1, 2, 3]))); // true\nconsole.log(isIterable({ a: 1 })); // falseExpected Result / What Changed\n- You can safely guard whether a target is "capable of `for...of`" at runtime. (MDN: Symbol.iterator)
5) Making an Object iterable "directly" ( Symbol.iterator)
When you truly need the for...of obj form, you can implement Symbol.iterator.
However, you must decide for yourself "what and in what order to iterate."
Below is an example of an "array-like" object with numeric keys made to iterate over its values. (A length property is added to make the rules clear.)
const obj = { 0: 1, 1: 2, 2: 3, 3: 4, length: 4 };\n\nobj[Symbol.iterator] = function* () {\n for (let i = 0; i < this.length; i++) {\n yield this[i];\n }\n};\n\nfor (const value of obj) {\n console.log(value);\n}Expected Result / What Changed\n- obj becomes iterable, enabling for...of obj.\n- The iteration rules (0 to length-1, values being this[i]) are fixed by the code. (MDN: Iteration protocols)
Verification (Checklist)
for...of is iterable using isIterable()?entries/values/keys that fits the purpose for Object iteration?Symbol.iterator is implemented, are the "iteration rules (order/end condition/return value)" clear?for...in, did you guard so that prototype chain properties don't get mixed in? (MDN: for…in)Common Mistakes/FAQ
Q1. Does spread ( ...) also follow the iterable protocol?
It depends on the situation.
- Spread in arrays/function calls like
const a = [...set]orfn(...arr)requires an iterable. (MDN: Spread syntax) {...obj}like Object spread does not use an iterator; it copies own enumerable properties. It can work even if the Object is not iterable. (MDN: Spread syntax)
Expected Result / What Changed\n- You can avoid the misunderstanding that "spread is always based on iteration."
Q2. Isn't it cleaner to attach Symbol.iterator to an Object?
It might look clean in short code. However, there are things to watch out for.
- If iteration rules are not unified across the team/project, the cost of code interpretation increases.
- It may be unclear whether you are iterating over "keys/values/pairs" just by looking at the code.
Most of the time, it is safer to reveal intent through conversions like Object.entries(). (MDN: Object.entries)
Q3. Should I not use for...in?
You can use it depending on the purpose. However, for...in is for key enumeration, and prototype chain properties can get mixed in. So, a guard that passes only own properties is usually placed.
for (const key in obj) {\n if (!Object.hasOwn(obj, key)) continue;\n // ...\n}Conclusion
for...of only iterates over iterables. So, Array, Set, and Map work, but plain Objects do not.
Object iteration is fundamentally handled by revealing intent via Object.entries/values/keys, and Symbol.iterator is implemented only when the for...of obj form is absolutely necessary to define custom iteration rules.