Command Palette

Search for a command to run...

Hectal
PHASE 6Beginner ~24 min· topic 4 of 7

Topic 6.4

Looping Over Objects

In one line

Objects aren't directly loopable with for...of, so you turn them into arrays first: Object.keys for names, Object.values for values, Object.entries for [key, value] pairs; Object.fromEntries turns pairs back into an object.

Think of it like this

A class register. You can't "walk along" a register like a queue, but you can ask for a list of the names, a list of the marks, or a list of name-and-mark pairs, and then go down that list one line at a time. When you've worked out new marks, you can write the pairs back into a fresh register. That's keys, values, entries and fromEntries.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Iterable
Something for...of can walk through one item at a time, like an array or a string. Plain objects are not.
Object.keys
Gives an array of an object's own keys.
Object.values
Gives an array of an object's own values.
Entry
A two-item array [key, value] that describes one property.
Object.entries
Gives an array of entries, one per own property.
Object.fromEntries
Builds a new object from a list of [key, value] pairs.
for...in
An old loop over an object's keys that also visits inherited keys.

Step by step

01Turn the object into arrays

Each of the three methods gives a normal array, so length, indexes and every array method work on it.

app.jswhole filejavascript
const marks = { maths: 92, science: 85 };
console.log(Object.keys(marks));    // [ 'maths', 'science' ]
console.log(Object.values(marks));  // [ 92, 85 ]
console.log(Object.entries(marks)); // [ [ 'maths', 92 ], [ 'science', 85 ] ]

02Loop over the entries

Destructure each entry into a key and a value right in the loop header.

app.jswhole filejavascript
for (const [subject, mark] of Object.entries(marks)) {
  console.log(subject + ": " + mark);
}

03Transform and rebuild

To change every value, or keep only some keys, go through entries, use array methods, and rebuild with fromEntries. The original object is untouched.

Transform and rebuilddiagram
Rendering diagram…

04Count things into an object

An object is a natural tally sheet: one key per thing, one number per key. ?? 0 starts a key at zero the first time it's seen.

app.jswhole filejavascript
const votes = ["tea", "coffee", "tea"];
const tally = {};
for (const v of votes) {
  tally[v] = (tally[v] ?? 0) + 1;
}
console.log(tally); // { tea: 2, coffee: 1 }

Try it yourself

  1. 1

    Add a subject

    In the first example, add art: 90 to marks. Predict the new average before you run it.

  2. 2

    Double the prices

    In the second example, change the map so every price is doubled. Predict the first line.

  3. 3

    Loop the object directly

    In the first example, change Object.entries(marks) to marks in the for...of. Predict what the console says.

Code & diagrams

keys, values and entries ES2017+ New tab

`Object.values` and `Object.entries` are ES2017; `Object.keys` is older (ES5). Each row's text is set with `textContent`, which is safe for any data (Phase 8.3).

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

[ 'maths', 'science', 'english' ]
[ 92, 85, 78 ]
[ [ 'maths', 92 ], [ 'science', 85 ], [ 'english', 78 ] ]
Average: 85
Map and filter an object with fromEntries ES2019+ New tab

`[, price]` skips the first item of each pair (the key). `URLSearchParams` gives pairs too, so `fromEntries` turns a query string into an object; every value is a string.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

{ tea: 25, samosa: 10, juice: 45, cake: 75 }
{ tea: 30, samosa: 15 }
{ name: 'Asha', age: '14' }
for...in sees inherited keys New tab

`Object.create(base)` makes an object whose prototype is `base` (Phase 7.3). `for...in` walks up to it; `Object.keys` doesn't.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

for...in: own
for...in: shared
Object.keys: [ 'own' ]
own only: own
Counting words New tab

Sorting the entries by their second item ranks the words. `Object.groupBy` is ES2024: every word here has three letters, so there's only one group.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

{ the: 3, cat: 1, and: 2, hat: 1, bat: 1 }
[ 'the', 3 ] [ 'and', 2 ]
[ 'three letters' ]

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

for...of on a plain object

In the first example, write for (const [subject, mark] of marks).

terminal
$ Console
── what you'll see ──
Uncaught TypeError: marks is not iterable

Break #2

Forget to destructure the pair

In the first example, write for (const subject of Object.entries(marks)) and use subject as the text.

terminal
$ the page shows
── what you'll see ──
The Subject column shows text like `maths,92` and the average becomes NaN.

Myth vs fact

Myth

for...in is the right way to loop over an array.

Fact

It gives string keys ("0", "1"), includes any extra or inherited enumerable properties, and its purpose is objects. Use for...of, forEach or a counting loop for arrays.

Myth

Object.keys lists every property an object has.

Fact

It lists own, enumerable, string-keyed properties only. Inherited, non-enumerable (Topic 6.7) and symbol keys are left out. Reflect.ownKeys lists all own keys, including symbols and non-enumerable ones.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Object.entries allocates a new array of new arrays every call. In a hot path over a large object, for...in with an Object.hasOwn check, or keeping the data in a Map (which iterates without copying), avoids those allocations.

  • ▸

    Object.groupBy (ES2024) returns an object with a null prototype, so a group named toString or __proto__ can't clash with inherited members. Map.groupBy does the same into a Map when the group keys aren't strings.

  • ▸

    Object.fromEntries uses define semantics: a "__proto__" key becomes an ordinary own property, unlike assignment, which sets the prototype. That makes it safer than obj[key] = value loops for untrusted keys, a defence against prototype pollution (an attack where input like __proto__ changes Object.prototype for the whole app).

  • ▸

    If a later entry has the same key as an earlier one, fromEntries keeps the later value, like writing the key twice in a literal.

Remember this

  1. 1

    A plain object is not iterable: for (const x of obj) throws TypeError: obj is not iterable. (Iterable means "something for...of and spread [...x] can walk through", like arrays and strings; Phase 14.1 shows how it works.) To loop over an object, ask for an array that describes it and loop over that.

  2. 2

    Object.keys(obj) returns an array of the object's own enumerable string keys. Object.values(obj) (ES2017) returns their values. Object.entries(obj) (ES2017) returns an array of entries: two-item arrays [key, value]. All three use the same order (integer-like keys first, then insertion order; Topic 6.1) and skip inherited properties, non-enumerable properties and symbol keys.

  3. 3

    Object.entries pairs naturally with array destructuring in a for...of loop: for (const [key, value] of Object.entries(obj)) { ... }. Each entry is unpacked into two variables. Because the result is a normal array, you can also use every array method from Phase 5 on it: filter, map, sort, reduce.

  4. 4

    Object.fromEntries(pairs) (ES2019) does the reverse: it builds an object from a list of [key, value] pairs. The pattern entries → array methods → fromEntries is how you "map" or "filter" an object: Object.fromEntries(Object.entries(prices).filter(([, p]) => p < 40)). It accepts any iterable of pairs, so it also converts a Map or URLSearchParams (the query part of a URL) into an object.

  5. 5

    The older **for...in loop**, for (const key in obj), visits each enumerable string key, including inherited ones from the prototype chain. With plain objects that rarely matters, but with objects created from other objects (Phase 7.3) or old libraries that added to Object.prototype, it shows extra keys. That's why modern code prefers Object.keys/entries, or guards for...in with Object.hasOwn. Never use for...in on arrays: it gives string indexes and any extra properties.

  6. 6

    Changing an object while looping over its Object.keys array is safe, because that array was made before the loop started. A common job is counting: start with {} and do counts[word] = (counts[word] ?? 0) + 1 for each item. ES2024 added Object.groupBy(items, fn), which groups an array into an object of arrays in one call.

Explain it without notes

01

Why can't you use for...of directly on a plain object, and what do you use instead?

02

What do Object.keys, Object.values and Object.entries return, and what do they leave out?

03

How do you filter or transform an object's properties?

04

What's the difference between for...in and Object.keys?

Practice

01

Given const stock = { pens: 12, books: 0, bags: 3, rulers: 0 }, log an array of the names of items that are out of stock.

02

Write invert(obj) that swaps keys and values: invert({ a: "x", b: "y" }) returns { x: 'a', y: 'b' }.

03

Given an array of fruit names with repeats, build an object counting each fruit, then log the total number of fruits using Object.values.

Trade-offs

  • ↔

    Object.entries + array methods + fromEntries is clear and never mutates, but makes several temporary arrays; a single for...in or for...of loop writing into a new object is faster for very large objects.

  • ↔

    Counting into a plain object is simple when keys are strings; when keys can be anything, or names like constructor might appear, a Map or Object.create(null) avoids clashes with inherited names.

Done when you can

  • I can get an object's keys, values and entries as arrays.

  • I can loop over an object with for...of and Object.entries, destructuring each pair.

  • I can filter or transform an object with entries and Object.fromEntries.

  • I can count things into an object.

  • I can explain why for...in may show inherited keys.