JavaScript (ES2026) Module 3 – Control flow
for…of vs for…in
What for…of and for…in visit in JavaScript: the values of arrays, strings, Maps and Sets against string keys, inherited ones too, and how to loop over objects.
What you will learn
- Iterate arrays, strings, Maps and Sets with for…of
- Explain what for…in actually visits and why it is wrong for arrays
- Read the keys, values and entries of objects safely
Before you start
What you’ll learn · Before you start
What you will learn
- Iterate arrays, strings, Maps and Sets with for…of
- Explain what for…in actually visits and why it is wrong for arrays
- Read the keys, values and entries of objects safely
Before you start
On this page
for…of and for…in look alike and do different jobs. for (const value of collection) walks through the
values of anything iterable: the elements of an array, the characters of a string, the entries of a Map and the
members of a Set. for (const key in object) walks through the keys of an object’s enumerable properties, as
strings, including properties the object inherits from its prototype. So use for…of for arrays and other
collections, and for a plain object combine it with Object.keys, Object.values or Object.entries. for…in is the
right tool almost only when you deliberately want the inherited properties as well. This lesson runs both loops over
the same kinds of data, reproduces the two classic for…in surprises, string keys and inherited keys, and explains
the order in which keys come out.
for…of: the values of a collection
// for…of gives the values of anything iterable: arrays, strings, Maps and Sets.
const scores = [52, 17, 88];
for (const score of scores) console.log('array value:', score);
for (const ch of 'a😀b') console.log('string character:', ch);
const runs = new Map([
['Asha', 52],
['Ravi', 17],
]);
for (const [player, total] of runs) console.log('map entry:', player, total);
const tags = new Set(['js', 'loops', 'js']); // a Set keeps each value only once
for (const tag of tags) console.log('set value:', tag); Output
array value: 52 array value: 17 array value: 88 string character: a string character: 😀 string character: b map entry: Asha 52 map entry: Ravi 17 set value: js set value: loops
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node iterate-everything.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
For an array, for…of gives the elements in order, from index 0 up. For a string it gives one character at a time.
For a Map, each value is a [key, value] pair, which const [player, total] takes apart, and the entries come in
the order they were added. A Set gives each of its members once, in the order they were first added, which is why
the second 'js' is not printed. Any object that is iterable, that is, one that provides an iterator through its
Symbol.iterator method, works the same way; the module on iterators and generators shows how to write one. A plain
object is not iterable:
// A plain object has no iterator, so for…of refuses it: loop over Object.entries(stock) instead.
const stock = { apples: 12, pears: 0 };
for (const item of stock) {
console.log(item);
} Output (exit status 1)
Printed as an error (standard error)
not-iterable.mjs:3
for (const item of stock) {
^
TypeError: stock is not iterable
at not-iterable.mjs:3:20
at ModuleJob.run (node:internal/modules/esm/module_job:561:25)
at async node:internal/modules/esm/loader:647:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
Node.js v24.21.0
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node not-iterable.mjs
The fix is one of the three methods in the section on objects below: for (const [item, count] of Object.entries(stock)).
Strings: characters, not code units
// A string's length counts UTF-16 code units. for…of walks code points, which keeps most emoji in one piece.
const words = ['cat', 'a😀b', '👍🏽', '👨👩👧'];
for (const word of words) {
let codePoints = 0;
for (const ch of word) codePoints++;
console.log(`${word}: length ${word.length}, for…of iterations ${codePoints}`);
}
// An index reads one code unit, so it gets only half of the emoji; for…of gets all of it.
const smile = 'a😀b';
let second;
let position = 0;
for (const ch of smile) {
if (position === 1) second = ch;
position++;
}
console.log('smile[1] is the emoji?', smile[1] === '😀', '| for…of character 1 is the emoji?', second === '😀'); Output
cat: length 3, for…of iterations 3 a😀b: length 4, for…of iterations 3 👍🏽: length 4, for…of iterations 2 👨👩👧: length 8, for…of iterations 5 smile[1] is the emoji? false | for…of character 1 is the emoji? true
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node string-units.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
A string’s length counts UTF-16 code units, the 16-bit pieces that JavaScript strings are made of, and an emoji such
as 😀 needs two of them. for…of uses the string’s own iterator, which the specification defines to step through
code points, so it keeps 😀 in one piece: three iterations for 'a😀b', whose length is 4. An index does not:
smile[1] is only the first half of the emoji. Code points are still not always what a reader sees as one character.
👍🏽 is a thumbs-up followed by a skin-tone modifier, two code points, and the family emoji is three people joined by
two invisible characters, five code points in all. Counting what a reader sees, which the Unicode standard calls
grapheme clusters, takes Intl.Segmenter, which the module on text covers.
for…in: the keys of an object
// for…in gives the keys of an object's enumerable properties, and the keys are always strings.
const stock = { apples: 12, pears: 0 };
for (const key in stock) console.log('object key:', key, '->', stock[key]);
const scores = [52, 17, 88];
for (const key in scores) console.log('array key:', key, typeof key);
// Because the keys are strings, adding 1 joins text instead of counting.
for (const key in scores) console.log('key + 1 =', key + 1); Output
object key: apples -> 12 object key: pears -> 0 array key: 0 string array key: 1 string array key: 2 string key + 1 = 01 key + 1 = 11 key + 1 = 21
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node for-in-keys.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
For an object, for…in gives the name of each property, and stock[key] reads its value. Pointed at an array, it
gives the indexes, but as strings: typeof key is 'string', and key + 1 joins text, '01', instead of adding 1.
That is the first reason not to walk arrays with for…in.
Inherited keys
The second reason is that for…in also visits enumerable properties that the object inherits:
// for…in also visits the enumerable properties an object inherits through its prototype chain.
const defaults = { theme: 'light', language: 'en' };
const settings = Object.create(defaults); // settings inherits from defaults
settings.fontSize = 16;
settings.theme = 'dark'; // an own property hides the inherited one of the same name
for (const key in settings) console.log('for…in:', key);
console.log('Object.keys:', Object.keys(settings).join(', '));
for (const key in settings) {
if (Object.hasOwn(settings, key)) console.log('own only:', key);
}
// Arrays inherit too: code that adds an enumerable method to Array.prototype changes every for…in over an array.
Array.prototype.last = function () {
return this[this.length - 1];
};
const marks = [40, 75];
const visited = [];
for (const key in marks) visited.push(key);
console.log('for…in over an array:', visited.join(', '));
delete Array.prototype.last; // undo the change to the built-in prototype Output
for…in: fontSize for…in: theme for…in: language Object.keys: fontSize, theme own only: fontSize own only: theme for…in over an array: 0, 1, last
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node for-in-trap.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
What for…in and Object.keys visit on an object with a prototype
Text description of the diagram
The diagram shows the settings object of for-in-trap.mjs and the object it inherits from.
- settings has two own properties: fontSize, 16, and theme, 'dark'.
- Its prototype, defaults, has theme, 'light', and language, 'en'. The own theme of settings hides the inherited one of the same name.
for (const key in settings) first visits the own keys, fontSize and theme, and then the keys of the prototype that it has not seen yet, which leaves only language: it gives fontSize, theme and language. Object.keys(settings) reads the own keys only and gives fontSize and theme. Object.values and Object.entries follow the same rule as Object.keys.
settings has two own properties and inherits from defaults, so for…in gives language as well, which
Object.keys leaves out. The own theme hides the inherited one of the same name, and for…in names each key only
once. When you do use for…in, filter its keys with Object.hasOwn(object, key), which is what ESLint’s guard-for-in
rule asks for.
The second half of the program is a classic accident. It adds a method to Array.prototype by plain assignment, which
makes the method enumerable, and from then on every for…in over every array also visits 'last'. Built-in methods
such as push are not enumerable, so they never show up; a method added carelessly by a library or an old polyfill
does. for…of is not affected, because it asks the array’s iterator for the elements instead of listing property
names.
Holes in arrays
The two loops also disagree about sparse arrays, which have holes where no element was ever stored:
// A sparse array has a hole where no element was ever stored. for…of reads it as undefined; for…in skips it.
const sparse = [1, , 3];
const values = [];
for (const value of sparse) values.push(String(value));
console.log('for…of values:', values.join(', '));
const keys = [];
for (const key in sparse) keys.push(key);
console.log('for…in keys:', keys.join(', ')); Output
for…of values: 1, undefined, 3 for…in keys: 0, 2
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node holes.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
for…of reads every index from 0 to length - 1 and gets undefined for the hole. for…in visits only the
properties that exist, so it skips index 1. ESLint’s recommended rule no-sparse-arrays reports the double comma that
made the hole.
Looping over an object’s own properties
// To loop over an object's own properties, ask for its keys, values or entries, and use for…of on that array.
const runs = { Asha: 52, Ravi: 17, Meera: 88 };
for (const name of Object.keys(runs)) console.log('key:', name);
let total = 0;
for (const value of Object.values(runs)) total += value;
console.log('total runs:', total);
for (const [name, value] of Object.entries(runs)) console.log(`${name} scored ${value}`);
// The order: keys that look like array indexes first, from small to large, then the others in the order they were added.
const lockers = { b12: 'Asha', 7: 'Ravi', a3: 'Meera', 2: 'Tom' };
console.log('locker keys:', Object.keys(lockers).join(', ')); Output
key: Asha key: Ravi key: Meera total runs: 157 Asha scored 52 Ravi scored 17 Meera scored 88 locker keys: 2, 7, b12, a3
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node object-loops.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Object.keys, Object.values and Object.entries return arrays built from an object’s own enumerable properties
with string keys, so for…of can walk them without inherited surprises: the names, the values, or [name, value]
pairs to take apart. These three, for…in and JSON.stringify all list keys in the same order, which the
specification fixes. First come the keys that are array indexes, whole numbers such as '2' and '7' written without
leading zeros, in ascending numeric order; then the other string keys, in the order they were created. That is why
'2' and '7' jump ahead of 'b12' and 'a3'. When the order of entries matters and the keys can look like
numbers, use a Map, which keeps every key in the order it was added.
Changing an array while for…of runs
// for…of over an array checks the array's current length at every step, so changing the array changes the loop.
const queue = [2, 4, 5, 6];
for (const n of queue) {
if (n % 2 === 0) queue.splice(queue.indexOf(n), 1); // removes an even number while the loop is running
}
console.log('even numbers removed during the loop:', queue.join(', '));
// Build a new array instead: the loop reads one array and writes another.
const numbers = [2, 4, 5, 6];
const odd = [];
for (const n of numbers) {
if (n % 2 !== 0) odd.push(n);
}
console.log('odd numbers collected in a new array:', odd.join(', ')); Output
even numbers removed during the loop: 4, 5 odd numbers collected in a new array: 5
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node change-while-looping.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
The array’s iterator keeps an index and reads the array’s current length at every step. Removing the 2 moves 4 into
index 0, which the loop has already visited, so 4 is never tested and stays in the array. Adding elements has the
opposite effect: a loop that pushes onto the array it is walking keeps finding new elements and may never end. Read
one array and build another, as the second half does, or loop over a copy, for (const n of [...queue]). The module
on arrays shows filter, which does the same in one line.
for…of or for…in?
| Criterion | for…of | for…in |
|---|---|---|
| Each iteration gives | a value | a property name, as a string |
| Works on | iterables: arrays, strings, Maps, Sets | any object; arrays only through their index keys |
| Inherited properties | never: it uses the iterator | included when enumerable |
| Order | the iterator's: index order, or insertion order for a Map or Set | index keys ascending, other keys in creation order, then inherited keys |
| Holes in an array | visited, as undefined | skipped |
| A plain object | TypeError: not iterable; loop over Object.entries(obj) | its own and inherited enumerable keys |
| When to choose | Arrays, strings, Maps and Sets, and objects through Object.keys, values or entries | Rarely: when you want inherited enumerable keys as well, with Object.hasOwn to filter them |
Exercises
Exercise · Easy · JavaScript
Total an object of scores and find a Map's top scorer
Write two functions in scores.mjs:
sumScores(scoreboard)adds up the runs in an object whose keys are player names and whose values are their runs:sumScores({ Asha: 52, Ravi: 17 })is 69, and an empty object gives 0. Count the object's own properties only: a property it inherits from its prototype is not a player.topScorer(runs)takes aMapfrom player name to runs and returns the name with the most runs. When several players share the top score, return the one that was added to theMapfirst. An emptyMapgivesnull, and a player with 0 runs can still be the top scorer.
Starter code · scores.mjs
/**
* The total of the runs in an object of player name → runs (own properties only).
* @param {Record<string, number>} scoreboard
* @returns {number}
*/
export function sumScores(scoreboard) {
return 0;
}
/**
* The name of the player with the most runs in a Map of player name → runs (the first added on a tie), or null.
* @param {Map<string, number>} runs
* @returns {string | null}
*/
export function topScorer(runs) {
return null;
} The sample tests · scores.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { sumScores, topScorer } from './scores.mjs';
test('sumScores adds up every player', () => {
assert.equal(sumScores({ Asha: 52, Ravi: 17 }), 69);
assert.equal(sumScores({ Asha: 52, Ravi: 17, Meera: 88 }), 157);
});
test('sumScores of an empty object is 0', () => {
assert.equal(sumScores({}), 0);
});
test('sumScores counts own properties only', () => {
const board = Object.create({ extra: 1000 });
board.Asha = 10;
board.Ravi = 5;
assert.equal(sumScores(board), 15);
});
test('topScorer finds the most runs', () => {
const runs = new Map([
['Asha', 52],
['Ravi', 17],
['Meera', 88],
]);
assert.equal(topScorer(runs), 'Meera');
});
test('on a tie, the player added first wins', () => {
const runs = new Map([
['Ravi', 60],
['Asha', 60],
['Tom', 12],
]);
assert.equal(topScorer(runs), 'Ravi');
});
test('an empty Map has no top scorer', () => {
assert.equal(topScorer(new Map()), null);
});
test('a player with 0 runs can be the top scorer', () => {
assert.equal(topScorer(new Map([['Tom', 0]])), 'Tom');
assert.equal(
topScorer(
new Map([
['Ira', 0],
['Tom', 0],
]),
),
'Ira',
);
}); A hint
For sumScores, Object.values(scoreboard) lists the values of the object's own properties, inherited ones left out, and a for…of loop can add them up. For topScorer, for (const [name, score] of runs) visits the entries in the order they were added; keep the best name so far and replace it only when a score is strictly higher, and start from a best score that every real score beats.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
Fresher · What is the difference between for…of and for…in in JavaScript?
for…of iterates over the values of an iterable object: the elements of an array, the characters (code points) of a
string, the [key, value] entries of a Map and the members of a Set, in the order its iterator defines. for…in
iterates over the keys of an object’s enumerable string-keyed properties, including the ones it inherits through its
prototype chain, and always gives them as strings. So for…in over an array gives '0', '1' and so on, and also any
enumerable property that code added to Array.prototype, while for…of gives the elements themselves. A plain object
is not iterable, so for…of throws a TypeError for it; loop over Object.keys, Object.values or Object.entries
of the object instead. Use for…in only when inherited keys are wanted, and filter it with Object.hasOwn otherwise.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
function total(items) { let sum = 0; for (const i in items) sum += items[i].price; return sum; } passes its tests, but on one page of the app it returns NaN for the same cart. What could cause it, how would you confirm it, and how do you fix the function?
-
Mid to senior
A leaderboard is kept in an object keyed by player id, such as { u42: …, 1007: …, 7: … }, filled in the order the players joined. On screen, players 7 and 1007 always come first, in that order. Explain the rule behind the order, and choose a data structure that keeps the order you actually need.
Key takeaways
for…ofgives values: elements of an array, code points of a string,[key, value]pairs of aMap, members of aSet. It throws aTypeErrorfor a plain object.for…ingives keys as strings, inherited enumerable ones included, so it is the wrong loop for arrays.- For an object, loop over
Object.keys,Object.valuesorObject.entrieswithfor…of; withfor…in, filter withObject.hasOwn. - Keys that are array indexes come first in ascending order, then other keys in creation order; a
Mapkeeps the order in which keys were added. - A string’s
lengthcounts UTF-16 code units;for…ofkeeps 😀 whole, but a skin-tone or family emoji is several code points. - Do not add or remove elements of an array while
for…ofwalks it: build a new array or loop over a copy.
References
- ECMAScript 2026 Language Specification: The for-in, for-of, and for-await-of Statements (Ecma International)
- ECMAScript 2026 Language Specification: EnumerateObjectProperties (Ecma International)
- ECMAScript 2026 Language Specification: OrdinaryOwnPropertyKeys (Ecma International)
- ECMAScript 2026 Language Specification: String.prototype [ %Symbol.iterator% ] (Ecma International)
- ECMAScript 2026 Language Specification: %ArrayIteratorPrototype%.next (Ecma International)
- for...of (MDN Web Docs)
- for...in (MDN Web Docs)
- Object.entries() (MDN Web Docs)
- Object.hasOwn() (MDN Web Docs)
- Intl.Segmenter (MDN Web Docs)
- Unicode Standard Annex #29: Unicode Text Segmentation (Unicode Consortium)
- ESLint rule: guard-for-in (ESLint (OpenJS Foundation))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress