Blog
javascript
4 min read

Iteration is a Contract: Making Plain Objects Iterable with Symbol.iterator

An iterable is a contract for producing values in order. Adding Symbol.iterator to a plain object makes for...of and spread consume it consistently.

Iteration is a Contract: Making Plain Objects Iterable with Symbol.iterator
Cover
Cover

Iteration is a Contract: Making Plain Objects Iterable with Symbol.iterator

One-sentence takeaway: An iterable is a contract for producing values in order. Implementing Symbol.iterator on a plain object makes for...of and the spread operator consume it consistently.

Background / Problem

A plain object (Object) is great for key–value storage, but it isn't iterable by default. That often leads to patterns like:

  • Converting with Object.keys() / Object.values() whenever you need ordering.
  • Duplicating iteration rules across call sites, which increases maintenance mistakes.

The key point is simple: move the iteration rule into the object so the call site can stay uniform.

Core Concepts

Iterable and Iterator

  • Iterable: an object that provides Symbol.iterator, which returns an iterator.
  • Iterator: an object with next(). Each next() returns { value, done }.

For the exact contract, refer to official docs:

다이어그램 불러오는 중...

→ Expected outcome / What changes: the call site only uses iteration syntax (for...of, spread), while the object owns ordering and termination.

Approach

You can reach the goal ("make a plain object iterable") in a few ways:

1) Implement Symbol.iterator directly

  • Why: to fully control ordering/filtering/termination
  • Expected: call sites unify into for...of / spread

2) Convert with Object.values() and iterate

  • Why: fastest way to get something working
  • Expected: simple, but conversion logic stays scattered

3) Use a generator

  • Why: focus on "what to yield" without manual done handling
  • Expected: better readability as logic grows

Below, we start with the direct protocol implementation, then show the generator alternative.

Implementation (Code)

1) Minimal: add Symbol.iterator to a plain object

javascript
const obj = { 0: 1, 1: 2, 2: 3, 3: 4 };

obj[Symbol.iterator] = function () {
  let i = 1;
  return {
    next() {
      return i > 4 ? { done: true } : { value: i++, done: false };
    },
  };
};

console.log("--- for...of ---");
for (const value of obj) console.log("value:", value);

console.log("--- spread ---");
console.log(...obj);

→ Expected outcome / What changes: for...of obtains an iterator via Symbol.iterator() and repeatedly calls next() until done: true. Spread consumes values the same way.

2) Practical: iterate over the object's own values

javascript
const obj = { 0: 1, 1: 2, 2: 3, 3: 4 };

obj[Symbol.iterator] = function () {
  const values = Object.values(this);
  let idx = 0;
  return {
    next() {
      if (idx >= values.length) return { done: true };
      return { value: values[idx++], done: false };
    },
  };
};

console.log([...obj]); // [1, 2, 3, 4]

→ Expected outcome / What changes: iteration is anchored to the object's data, so call sites can stay consistent.

3) Alternative: a generator-based iterator

javascript
const obj = { 0: 1, 1: 2, 2: 3, 3: 4 };

obj[Symbol.iterator] = function* () {
  for (const v of Object.values(this)) yield v;
};

console.log([...obj]); // [1, 2, 3, 4]

→ Expected outcome / What changes: no manual next()/done bookkeeping—only the yield order remains.

Verification Checklist

for...of works.
Spread (...obj) outputs the same order.
Termination happens via done: true.
Ordering/filtering/termination rules live inside the object.
Use a generator if the iterator logic grows.

Common Pitfalls / FAQ

Q1. How is this different from for...in?

for...in enumerates property keys; for...of consumes iterable values.

Q2. Is adding Symbol.iterator to a plain object always a good idea?

Depending on team conventions or library assumptions, it can break the expectation that "plain objects are not iterable." In such cases, use a wrapper object or restrict the behavior to specific types.

Q3. Is Object.values() order always safe to rely on?

It depends on property ordering rules. If order is fundamental to the model, prefer structures where order is part of the design (like arrays or Map).

Summary

  • An iterable is a contract for producing values.
  • Symbol.iterator is the entry point; next() drives consumption.
  • Encapsulating iteration rules inside the object simplifies call sites.
  • Generators can keep complex iteration logic readable.

Conclusion

When iteration logic is scattered across call sites, mistakes grow with it. Implementing Symbol.iterator lets the object own "how to iterate," leaving only iteration syntax at the call site.

References

Related Posts