Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 16 minutes
  • Examples run with Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free lesson, Pro answers: 16 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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 over an array, a string, a Map and a Set JavaScript · iterate-everything.mjs
// 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

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:

for…of over a plain object JavaScript · not-iterable.mjs
// 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

length against for…of JavaScript · string-units.mjs
// 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

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.

Unicode Character Inspector Paste 👍🏽 or 👨‍👩‍👧 to see every code point that for…of steps through, the invisible joiners included.

for…in: the keys of an object

for…in over an object and an array JavaScript · for-in-keys.mjs
// 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

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:

Inherited keys in for…in JavaScript · for-in-trap.mjs
// 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

Diagram: for…in visits the own keys of settings, then the inherited keys of defaults; Object.keys reads only the own keys.for (key in settings)fontSize, theme, languageObject.keys(settings)fontSize, themesettings (own)defaults (prototype)fontSize: 16theme: 'dark'theme: 'light' (hidden)language: 'en'1. own keysown keys onlyinherits from2. keys notseen yet

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 in both loops JavaScript · holes.mjs
// 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

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

Object.keys, values and entries with for…of JavaScript · object-loops.mjs
// 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

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

Removing elements in the middle of a loop JavaScript · change-while-looping.mjs
// 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

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.

JavaScript Linter Paste for-in-keys.mjs: the Strict rules add guard-for-in to every for…in loop that does not filter its keys.

for…of or for…in?

for…of and for…in
Criterion for…offor…in
Each iteration gives a valuea property name, as a string
Works on iterables: arrays, strings, Maps, Setsany object; arrays only through their index keys
Inherited properties never: it uses the iteratorincluded when enumerable
Order the iterator's: index order, or insertion order for a Map or Setindex keys ascending, other keys in creation order, then inherited keys
Holes in an array visited, as undefinedskipped
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 entriesRarely: 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 a Map from 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 the Map first. An empty Map gives null, 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.

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.

  1. Question 1 of 5 What does for-in-trap.mjs print?

    What does this program print? Choose one answer.

    // 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
    Show the answer to question 1

    Answer: it prints

    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

    for…in visits the own keys first, fontSize and theme, then the inherited keys it has not seen, so the hidden theme of defaults is skipped and only language is added. Object.keys and the Object.hasOwn check keep the own keys only. Over the array, for…in gives the keys 0 and 1, not the values, and also the enumerable method last that the program added to Array.prototype.

  2. Question 2 of 5 Which loop gives you the entries of a Map called prices, in the order they were added?

    Choose one answer.

    Show the answer to question 2

    Answer: for (const [item, price] of prices)

    A Map is iterable, and for…of over it gives [key, value] pairs in insertion order, which destructuring splits into item and price. for…in looks at the Map object's own enumerable properties, and a Map keeps its entries somewhere else, so it visits nothing; Object.keys(prices) is an empty array for the same reason.

  3. Question 3 of 5 What does this print?

    Read the code, then choose one answer.

    for (const i in ['x', 'y']) {
      console.log(i + 1);
    }
    Show the answer to question 3

    Answer: 01 11

    for…in gives the keys of the array as strings, '0' and '1', and + with a string joins text, so the results are '01' and '11'. for…of would give the values 'x' and 'y'; a counting for loop would give the numbers 0 and 1.

  4. Question 4 of 5 In what order does Object.keys({ b: 1, 10: 2, a: 3, 2: 4 }) list the keys?

    Choose one answer.

    Show the answer to question 4

    Answer: '2', '10', 'b', 'a'

    Keys that look like array indexes come first, in ascending numeric order, so '2' comes before '10'. The other string keys follow in the order they were created: b, then a.

  5. Question 5 of 5 How many times does the body of for (const ch of 'hi😀') { … } run?

    Type a number.

    Show the answer to question 5

    Answer: 3 times

    The string's length is 4, because the emoji needs two UTF-16 code units, but for…of walks code points, so the emoji is one character and the body runs three times: h, i and the emoji.

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.

  1. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

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

    Model answer, Pro. First step and follow-ups

Key takeaways

  • for…of gives values: elements of an array, code points of a string, [key, value] pairs of a Map, members of a Set. It throws a TypeError for a plain object.
  • for…in gives keys as strings, inherited enumerable ones included, so it is the wrong loop for arrays.
  • For an object, loop over Object.keys, Object.values or Object.entries with for…of; with for…in, filter with Object.hasOwn.
  • Keys that are array indexes come first in ascending order, then other keys in creation order; a Map keeps the order in which keys were added.
  • A string’s length counts UTF-16 code units; for…of keeps 😀 whole, but a skin-tone or family emoji is several code points.
  • Do not add or remove elements of an array while for…of walks it: build a new array or loop over a copy.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.