JavaScript has always offered multiple ways to loop through data. From the classic numeric for loop inherited from C,
to the forEach method,
developers have had plenty of options. However, ES2015 introduced a
game-changer: the for...of loop.
This loop is still underutilized by many developers, yet it elegantly replaces most traditional iteration patterns. It works seamlessly with arrays, strings, Maps, Sets, NodeLists, and any object that implements the iterable protocol. More importantly, it gives you full control over how much of an iterable you consume, making it perfect for lazy evaluation and infinite sequences.
A Brief History of JavaScript Loops
Before diving into for...of, let's quickly
review the loops JavaScript has offered over the years:
The Numeric for Loop
The traditional for
loop, borrowed from C and similar languages, uses an index-based approach:
for (var index = 0, len = items.length; index < len; ++index) {
// Work with items[index]
}
While powerful, this syntax can be verbose and error-prone. You need to manage the index manually, cache the length for performance, and remember to increment the counter.
The for...in Loop
The for...in loop is
specifically designed to iterate over enumerable properties of an object:
var person = { first: 'John', last: 'Smith' };
for (var prop in person) {
console.log(prop, '=', person[prop]);
// Outputs: 'first' = 'John', 'last' = 'Smith'
}
Note that for...in is
not for iterating over array values—it's for object properties. Using it on arrays can lead to unexpected results.
While and do...while Loops
These conditional loops are present in many programming languages. while evaluates the
condition before each iteration, while do...while evaluates it
after, guaranteeing at least one execution.
What is for...of?
ES2015 formalized a crucial concept: iterables. An iterable is an object that implements the
iterable protocol, defined through the Symbol.iterator method.
Many built-in JavaScript objects are iterable: arrays, strings, Maps, Sets, NodeLists, and more.
The for...of loop is
the primary mechanism for consuming iterables where you have full control. You can consume exactly what you need,
and the amount can be determined dynamically or algorithmically.
When working with arrays, you typically care about the values, not the indices. The for...of loop makes this
natural:
// Old way: verbose and index-focused
for (var index = 0, len = items.length; index < len; ++index) {
var item = items[index];
// Work with item
}
// Modern way: clean and value-focused
for (const item of items) {
// Work with item directly
}
Like all loops, you can use break, continue, and return within for...of. But it's more
versatile than numeric loops: it works even without indices (like with Sets), and you don't need to worry about
caching the length.
Using const with for...of
Notice how we used const in the loop variable?
That's because we're not reassigning anything—we're working directly with each value as it comes from the
iterator. There's no index to manually increment, so const is the natural choice
and helps prevent accidental reassignments.
// ✅ Good: const prevents reassignment
for (const item of items) {
// item is read-only in this scope
}
// ❌ Avoid: let is unnecessary unless you need to reassign
for (let item of items) {
item = transform(item); // Only affects local variable, not the array
}
Destructuring on the Fly
When your iterator yields multiple values (like entries from a Map), you can destructure them directly in the loop declaration. This makes the code much cleaner:
const map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
// Without destructuring: verbose
for (const pair of map) {
console.log(pair[0], '=', pair[1]);
}
// With destructuring: clean and readable
for (const [key, value] of map) {
console.log(key, '=', value);
}
This pattern works with any iterable that yields arrays or tuples. You can destructure as deeply as needed:
const entries = [
['user', { id: 1, name: 'Alice' }],
['admin', { id: 2, name: 'Bob' }],
];
for (const [role, { id, name }] of entries) {
console.log(`${role}: ${name} (${id})`);
}
Complementary Iterators
Many iterables provide additional iterators beyond their default iteration. Most offer at least three: keys(), values(), and entries(). For objects
without keys (like Sets), the keys are simply the values themselves.
For example, if you want to iterate over an array while also getting the index, you can use entries():
const fruits = ['apple', 'banana', 'cherry'];
// Get both index and value
for (const [index, fruit] of fruits.entries()) {
console.log(`${index}: ${fruit}`);
}
// Output:
// 0: apple
// 1: banana
// 2: cherry
// Or iterate over just the keys (indices)
for (const index of fruits.keys()) {
console.log(index);
}
// Output: 0, 1, 2
// Or just the values (same as default)
for (const fruit of fruits.values()) {
console.log(fruit);
}
// Output: apple, banana, cherry
Maps and Sets also provide these methods, making it easy to work with their specific data structures:
const userMap = new Map([
['alice', { role: 'admin' }],
['bob', { role: 'user' }],
]);
// Iterate over entries (default)
for (const [username, user] of userMap) {
console.log(`${username}: ${user.role}`);
}
// Iterate over just keys
for (const username of userMap.keys()) {
console.log(username);
}
// Iterate over just values
for (const user of userMap.values()) {
console.log(user.role);
}
Lazy Evaluation: The Real Power
The true strength of for...of lies in its
ability to work with lazy evaluation. Because it consumes iterables incrementally rather than all
at once, it's the primary way to work with lazy computations, including infinite sequences.
Consider a generator that produces the Fibonacci sequence:
function* fibonacci() {
let [current, next] = [1, 1];
while (true) {
yield current;
[current, next] = [next, current + next];
}
}
This sequence never ends. If you try to convert it to an array with Array.from() or the spread
operator, your program will hang. However, you can safely consume it with for...of:
// Get first few terms via destructuring
const [a, b, c, d, e] = fibonacci();
// a === 1, b === 1, c === 2, d === 3, e === 5
// Consume until a condition is met
for (const term of fibonacci()) {
if (term > 100) break;
console.log(term);
}
// Output: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89
This pattern enables all sorts of lazy evaluation primitives. For example, here's a take function that limits
how many items to consume:
function* take(count, iter) {
if (count === 0) return;
for (const term of iter) {
yield term;
if (--count <= 0) break;
}
}
// Get first 10 Fibonacci numbers
for (const num of take(10, fibonacci())) {
console.log(num);
}
// Output: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
This lazy evaluation approach is fundamental to functional programming concepts and is similar to how libraries like RxJS work with observables.
Performance Considerations
You'll find all sorts of conflicting performance benchmarks online comparing different loop types. Most of them aren't representative of real-world scenarios. Keep these two points in mind:
- For most arrays (up to millions of elements), the performance difference is negligible.
- When not transpiled (using native browser support),
for...ofperforms similarly to other loops.
It's extremely rare that you'll need to fall back to a numeric for loop or while for performance
reasons. The readability and flexibility benefits of for...of far outweigh any
micro-optimizations in the vast majority of cases.
Replacing Old Patterns
You can now replace the vast majority of your traditional iterations with for...of. This includes:
- Numeric for loops when iterating over arrays
- forEach() when you need
breakorcontinue - jQuery/Lodash each() methods
- for...in when used incorrectly on arrays
// ❌ Old: forEach (can't break early)
items.forEach(item => {
if (item.shouldStop) return; // Only exits callback, not the loop
process(item);
});
// ✅ New: for...of (can break early)
for (const item of items) {
if (item.shouldStop) break; // Exits the entire loop
process(item);
}
// ❌ Old: jQuery each
$.each(items, function(index, item) {
// ...
});
// ✅ New: for...of
for (const item of items) {
// ...
}
Browser Support and Transpilation
The for...of loop is
natively supported in:
- Firefox 13+
- Chrome 38+
- Opera 25+
- Edge 12+
- Safari 7+
- Node.js 0.12+
For older environments, Babel and TypeScript can transpile for...of to compatible
code. On very large arrays (1M+ elements), transpiled code may have some performance overhead, but this depends
heavily on the JavaScript engine, your algorithm, and the context.
Real-World Examples
Example 1: Processing API Responses
async function processUsers() {
const response = await fetch('/api/users');
const users = await response.json();
for (const user of users) {
if (user.status === 'inactive') continue;
await updateUserProfile(user);
}
}
Example 2: Working with DOM Collections
// NodeList is iterable
const buttons = document.querySelectorAll('.action-button');
for (const button of buttons) {
button.addEventListener('click', handleClick);
}
// Works with HTMLCollection too (after conversion)
const divs = Array.from(document.getElementsByTagName('div'));
for (const div of divs) {
div.classList.add('processed');
}
Example 3: Custom Iterables
class NumberRange {
constructor(start, end) {
this.start = start;
this.end = end;
}
*[Symbol.iterator]() {
for (let i = this.start; i <= this.end; i++) {
yield i;
}
}
}
const range = new NumberRange(1, 5);
for (const num of range) {
console.log(num);
}
// Output: 1, 2, 3, 4, 5
Best Practices
- Use const by default: Since you're not reassigning the loop variable,
constis the natural choice. - Destructure when appropriate: If your iterable yields arrays or objects, destructure them directly in the loop declaration.
- Use entries() for indices: When you need both index and value from an array, use
array.entries(). - Prefer for...of over forEach: When you need
breakorcontinue,for...ofis your friend. - Leverage lazy evaluation: Use generators and
for...offor infinite sequences and on-demand computations.
Conclusion
The for...of loop is
one of JavaScript's most elegant and powerful features. It simplifies iteration, works with a wide variety of data
structures, and unlocks the power of lazy evaluation through generators.
While it may not be as widely adopted as it should be, for...of should be your
default choice for iteration in modern JavaScript. It replaces most numeric loops, eliminates the need for forEach in many cases, and
provides a clean, readable syntax that works across all iterable types.
Start using for...of in
your code today. Your future self (and your teammates) will thank you for the cleaner, more maintainable code.