Blog
frontend
6 min read

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...of iterates only over objects that follow the "Iteration protocols." (MDN: for…of)
  • Array, Set, Map, String are iterable by default, but plain Objects are not.
  • When iterating over Objects, they are usually converted using Object.entries()/values()/keys(), and Symbol.iterator is 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.

javascript
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 iterable

Expected 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 }

(MDN: Iteration protocols)


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 if true).

Because of this protocol, you can think of for...of working internally like this.

javascript
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.

  1. [key, value] pairs neededObject.entries(obj)
  2. Only values neededObject.values(obj)
  3. Only keys neededObject.keys(obj)
  4. Truly need the for...of obj form → Implement Symbol.iterator directly.

Implementation (Code)

1) Iterating over [key, value] using Object.entries()

javascript
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()

javascript
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.

javascript
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"

javascript
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 }));            // false

Expected 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.)

javascript
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)

Checked if the target for for...of is iterable using isIterable()?
Chosen the one among entries/values/keys that fits the purpose for Object iteration?
If Symbol.iterator is implemented, are the "iteration rules (order/end condition/return value)" clear?
If using 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] or fn(...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.

javascript
for (const key in obj) {\n  if (!Object.hasOwn(obj, key)) continue;\n  // ...\n}

(MDN: Object.hasOwn)


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.


References

Related Posts