Iterators & the for...of Protocol
Magic nahi, Protocol hai — Symbol.iterator se next() tak, aur early break pe cleanup tak.
An Iterable is any object that implements the [Symbol.iterator]() method. An Iterator is an object with a .next() method that returns { value, done }. They are two halves of one protocol — calling [Symbol.iterator]() on an iterable returns an iterator.
for...of, JavaScript simply calls [Symbol.iterator]() and keeps calling .next() until done is true. That's the entire mechanism — no magic, just a contract.How for...of actually works under the hood:
// How for...of actually works under the hood
const arr = ['a', 'b', 'c'];
// This:
for (const item of arr) {
console.log(item);
}
// Is equivalent to this:
const iterator = arr[Symbol.iterator]();
let result = iterator.next();
while (!result.done) {
const item = result.value;
console.log(item);
result = iterator.next();
}
// Calling .next() manually on an array iterator
const it = arr[Symbol.iterator]();
console.log(it.next()); // { value: 'a', done: false }
console.log(it.next()); // { value: 'b', done: false }
console.log(it.next()); // { value: 'c', done: false }
console.log(it.next()); // { value: undefined, done: true }Any object can be made iterable by adding a [Symbol.iterator]() method. This method must return an iterator object — an object with a .next() method. Each call to .next() must return { value, done }.
The iterator uses closure to maintain its state (current position). The [Symbol.iterator]() method creates a new scope where the state variable lives, and returns the iterator object that can access and update it.
// Custom iterable: range of numbers
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
// This returns the iterator object
let current = this.from;
const last = this.to;
return {
next() {
if (current <= last) {
return { value: current++, done: false };
} else {
return { value: undefined, done: true };
}
}
};
}
};
// Now we can use for...of on our custom object!
for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
}
// Spread also works!
const rangeArray = [...range]; // [1, 2, 3, 4, 5]
for...of encounters range, it calls range[Symbol.iterator](). That creates a fresh current variable in the closure. Each .next() call sees the updated current and returns the next value. This factory pattern means every for...of gets its own independent iterator.If a for...of loop is exited early (via break, return, or throw), the iterator's .return() method is called if it exists. This is used for cleanup — closing files, releasing resources, etc.
The .return() method must return an object like { done: true }. If the iterator doesn't have .return(), nothing happens on early exit — no error, just no cleanup.
const iterable = {
[Symbol.iterator]() {
let step = 0;
return {
next() {
step++;
if (step <= 5) return { value: step, done: false };
return { done: true };
},
return() {
console.log('Iterator closed early! Cleanup time.');
return { done: true };
}
};
}
};
// Normal exit — .return() is NOT called
for (const val of iterable) {
console.log(val); // 1, 2, 3, 4, 5
}
// Early exit — .return() IS called
for (const val of iterable) {
if (val === 2) break;
console.log(val); // 1
}
// Logs: "Iterator closed early! Cleanup time."
.return() is called on early exits from for...of loops. This is implemented by wrapping the loop body in a try/finally block internally. If you break or throw, the finally block invokes iterator.return(). This makes for...of safer than manual iterator consumption for resources.Array-like and Iterable are often confused, but they are completely different contracts:
Array-like: Has numeric indices and a .length property (e.g., arguments, NodeList). May NOT have [Symbol.iterator].
Iterable: Has [Symbol.iterator] method (e.g., Map, Set, strings). May NOT have .length.
Array.from() converts BOTH array-like AND iterable objects into real Arrays. But spread [...x] ONLY works with iterables — it FAILS on plain array-likes without the iterator protocol.// Array-like (has length and indices, but NO Symbol.iterator)
const arrayLike = {
0: 'Hello',
1: 'World',
length: 2
};
// [...arrayLike] // TypeError: arrayLike is not iterable!
// Fix 1: Array.from (works with array-likes AND iterables)
const arr1 = Array.from(arrayLike); // ['Hello', 'World'] ✅
// Fix 2: Add the iterator protocol manually
arrayLike[Symbol.iterator] = function() {
let i = 0;
return {
next: () => i < this.length ?
{ value: this[i++], done: false } :
{ done: true }
};
};
const arr2 = [...arrayLike]; // ['Hello', 'World'] ✅
// NodeList is both array-like AND iterable in modern browsers
// const nodes = document.querySelectorAll('div');
// [...nodes] // Works!Strings are iterable. for...of on a string iterates over code points, not UTF-16 code units. This fixes the emoji problem!
"🙂"[0] returns a broken surrogate half, but [..."🙂"] returns the whole emoji. The string iterator correctly handles surrogate pairs — characters outside the BMP (above U+FFFF).
const emoji = '🙂🎉';
// WRONG: Indexing splits surrogate pairs
console.log(emoji[0]); // broken high surrogate!
console.log(emoji.length); // 4 (two emojis = 4 UTF-16 code units!)
// RIGHT: String iterator handles surrogate pairs
const chars = [...emoji];
console.log(chars); // ['🙂', '🎉'] ✅
console.log(chars.length); // 2 (actual character count!)
// for...of also works correctly
for (const char of emoji) {
console.log(char); // '🙂', then '🎉'
}
// Manual string iterator
const strIter = emoji[Symbol.iterator]();
console.log(strIter.next()); // { value: '🙂', done: false }
console.log(strIter.next()); // { value: '🎉', done: false }
console.log(strIter.next()); // { value: undefined, done: true }
[...str] might not be enough — it handles surrogate pairs, but not combined characters like flags or skin-tone modifiers. Use Intl.Segmenter: new Intl.Segmenter('en', {granularity: 'grapheme'}).segment(str) for accurate visual character splitting.Lo kar liya — Key Points:
- ✅ Iterable: Object with
[Symbol.iterator]()method. Iterator: Object with.next()method returning{ value, done }. - ✅
for...ofcalls[Symbol.iterator]()and repeatedly calls.next()untildoneistrue. - ✅ Custom iterables: Add
[Symbol.iterator]()returning an object with.next()to make any object work withfor...ofand spread. - ✅ If a
for...ofloop exits early (break,throw), the iterator's.return()method is called for cleanup. - ✅ Array-like (has
length/indices) ≠ Iterable (hasSymbol.iterator).Array.fromworks with both; spread only works with iterables. - ✅ String iterators correctly handle surrogate pairs, making
[...emoji]the right way to split strings with emojis.
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login