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...ofcan 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.
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.
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.
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.
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
Add a subject
In the first example, add
art: 90tomarks. Predict the new average before you run it. - 2
Double the prices
In the second example, change the
mapso every price is doubled. Predict the first line. - 3
Loop the object directly
In the first example, change
Object.entries(marks)tomarksin thefor...of. Predict what the console says.
Code & diagrams
`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
Console
Nothing logged yet.Expected console output
[ 'maths', 'science', 'english' ]
[ 92, 85, 78 ]
[ [ 'maths', 92 ], [ 'science', 85 ], [ 'english', 78 ] ]
Average: 85`[, 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
Console
Nothing logged yet.Expected console output
{ tea: 25, samosa: 10, juice: 45, cake: 75 }
{ tea: 30, samosa: 15 }
{ name: 'Asha', age: '14' }`Object.create(base)` makes an object whose prototype is `base` (Phase 7.3). `for...in` walks up to it; `Object.keys` doesn't.
Preview
Console
Nothing logged yet.Expected console output
for...in: own
for...in: shared
Object.keys: [ 'own' ]
own only: ownSorting 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
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).
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.
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.entriesallocates a new array of new arrays every call. In a hot path over a large object,for...inwith anObject.hasOwncheck, or keeping the data in aMap(which iterates without copying), avoids those allocations. - ▸
Object.groupBy(ES2024) returns an object with a null prototype, so a group namedtoStringor__proto__can't clash with inherited members.Map.groupBydoes the same into aMapwhen the group keys aren't strings. - ▸
Object.fromEntriesuses define semantics: a"__proto__"key becomes an ordinary own property, unlike assignment, which sets the prototype. That makes it safer thanobj[key] = valueloops for untrusted keys, a defence against prototype pollution (an attack where input like__proto__changesObject.prototypefor the whole app). - ▸
If a later entry has the same key as an earlier one,
fromEntrieskeeps the later value, like writing the key twice in a literal.
Remember this
- 1
A plain object is not iterable:
for (const x of obj)throwsTypeError: obj is not iterable. (Iterable means "somethingfor...ofand 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
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
Object.entriespairs naturally with array destructuring in afor...ofloop: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
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 aMaporURLSearchParams(the query part of a URL) into an object. - 5
The older **
for...inloop**,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 toObject.prototype, it shows extra keys. That's why modern code prefersObject.keys/entries, or guardsfor...inwithObject.hasOwn. Never usefor...inon arrays: it gives string indexes and any extra properties. - 6
Changing an object while looping over its
Object.keysarray is safe, because that array was made before the loop started. A common job is counting: start with{}and docounts[word] = (counts[word] ?? 0) + 1for each item. ES2024 addedObject.groupBy(items, fn), which groups an array into an object of arrays in one call.
Explain it without notes
Why can't you use for...of directly on a plain object, and what do you use instead?
What do Object.keys, Object.values and Object.entries return, and what do they leave out?
How do you filter or transform an object's properties?
What's the difference between for...in and Object.keys?
Practice
Given const stock = { pens: 12, books: 0, bags: 3, rulers: 0 }, log an array of the names of items that are out of stock.
Write invert(obj) that swaps keys and values: invert({ a: "x", b: "y" }) returns { x: 'a', y: 'b' }.
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 +fromEntriesis clear and never mutates, but makes several temporary arrays; a singlefor...inorfor...ofloop 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
constructormight appear, aMaporObject.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...ofandObject.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...inmay show inherited keys.