JavaScript (ES2026) Module 5 – Objects in depth
Object.keys, entries, fromEntries and key order
Turn JavaScript objects into arrays of keys, values and entries, build them back with Object.fromEntries, and predict the order keys come out in.
What you will learn
- Convert objects to arrays of keys, values or entries and back
- Transform objects with entries, map and fromEntries
- Predict the order of an object's properties
Before you start
What you’ll learn · Before you start
What you will learn
- Convert objects to arrays of keys, values or entries and back
- Transform objects with entries, map and fromEntries
- Predict the order of an object's properties
Before you start
On this page
Objects are not arrays, so they have no map, filter or length. Three functions bridge the gap:
Object.keys(obj) returns an array of the object’s keys, Object.values(obj) an array of its values and
Object.entries(obj) an array of [key, value] pairs. All three list only the object’s own, enumerable,
string keys, never inherited or symbol keys. Object.fromEntries(pairs) goes the other way and builds an object
from pairs, so entries, an array method and fromEntries together transform a whole object in one expression. The
keys always come out in a fixed order that surprises many people: integer-like keys first, smallest first, then the
other string keys in the order they were added, then symbols. So Object.keys({ b: 1, 2: 1, a: 1, 1: 1 }) is
['1', '2', 'b', 'a']. This lesson converts a menu object both ways, raises every price in one line, spells out the
ordering rule with its edge cases, and shows why numeric ids in an object lose their order.
Keys, values and entries
// Object.keys, Object.values and Object.entries turn an object into an array you can loop over or transform.
const menu = { dosa: 80, idli: 40, vada: 35, filterCoffee: 30 };
console.log(Object.keys(menu));
console.log(Object.values(menu));
console.log(Object.entries(menu));
console.log('dishes:', Object.keys(menu).length);
for (const [dish, price] of Object.entries(menu)) {
if (price >= 40) console.log(`${dish} costs ${price}`);
}
console.log('one of each:', Object.values(menu).reduce((sum, price) => sum + price, 0));
// Only own, enumerable, string keys: inherited properties and symbol keys are left out.
const todaysMenu = Object.create(menu); // inherits every dish of menu
todaysMenu.pongal = 60;
todaysMenu[Symbol('note')] = 'chef special';
console.log(Object.entries(todaysMenu)); Output
[ 'dosa', 'idli', 'vada', 'filterCoffee' ] [ 80, 40, 35, 30 ] [ [ 'dosa', 80 ], [ 'idli', 40 ], [ 'vada', 35 ], [ 'filterCoffee', 30 ] ] dishes: 4 dosa costs 80 idli costs 40 one of each: 185 [ [ 'pongal', 60 ] ]
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node keys-values-entries.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(menu)gives the dish names andObject.values(menu)the prices, in matching order.Object.entries(menu)gives one[dish, price]pair per property, which is the most useful of the three: with destructuring,for (const [dish, price] of Object.entries(menu))reads both at once.Object.keys(menu).lengthis the usual way to count an object’s properties, because objects have nolength.todaysMenuinherits every dish frommenuand has one of its own,pongal, plus a symbol key.Object.entrieslists onlypongal: inherited keys and symbol keys are left out. That is usually what you want when an object holds data, and it is the difference from afor...inloop, which also visits inherited enumerable keys.
These functions take a snapshot. Adding or removing keys afterwards does not change an array you already have, and changing the array does not change the object, although a value that is itself an object is still shared by both.
Back to an object with Object.fromEntries
Object.fromEntries takes any iterable of [key, value] pairs, such as the array from Object.entries or a Map,
and builds a new plain object from it. That makes the round trip entries → array methods → fromEntries the standard
way to transform an object:
// Transforming an object: Object.entries, then array methods, then Object.fromEntries.
const menu = { dosa: 80, idli: 40, vada: 35, filterCoffee: 30 };
const raised = Object.fromEntries(Object.entries(menu).map(([dish, price]) => [dish, Math.round(price * 1.05)]));
console.log('5 % more:', raised);
const under40 = Object.fromEntries(Object.entries(menu).filter(([, price]) => price < 40));
console.log('under 40:', under40);
const dishByPrice = Object.fromEntries(Object.entries(menu).map(([dish, price]) => [price, dish]));
console.log('swapped:', dishByPrice); // the prices became keys, and number-like keys come out in numeric order
// Any iterable of [key, value] pairs works, a Map for example. A key that comes twice keeps its last value.
console.log(Object.fromEntries(new Map([['tea', 20], ['lassi', 50]])));
console.log(Object.fromEntries([['size', 'small'], ['dish', 'dosa'], ['size', 'large']]));
console.log('original:', menu); // nothing above changed it Output
5 % more: { dosa: 84, idli: 42, vada: 37, filterCoffee: 32 }
under 40: { vada: 35, filterCoffee: 30 }
swapped: { '30': 'filterCoffee', '35': 'vada', '40': 'idli', '80': 'dosa' }
{ tea: 20, lassi: 50 }
{ size: 'large', dish: 'dosa' }
original: { dosa: 80, idli: 40, vada: 35, filterCoffee: 30 }
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node price-update.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
- Change every value.
map(([dish, price]) => [dish, Math.round(price * 1.05)])raises each price by 5 % and rounds it, 35 becoming 37. - Keep some entries.
filter(([, price]) => price < 40)keeps two dishes; the empty slot before the comma skips the key in the destructuring pattern. - Swap keys and values. The prices become keys, and because they are integer-like they come out in numeric order: 30, 35, 40, 80. This is the ordering rule at work, covered in the next section.
- From other sources. A
Mapconverts directly. When a key appears twice, the later value wins and the key keeps the place where it first appeared, assizeshows.URLSearchParamsis an iterable of pairs too, soObject.fromEntries(new URLSearchParams(location.search))turns a page’s query string into an object.
The original menu is unchanged at the end: every step built a new array or a new object.
The order of keys
The specification fixes the order in which an object lists its own keys, in its OrdinaryOwnPropertyKeys steps:
- keys that are array indexes, in ascending numeric order;
- all other string keys, in the order they were created;
- symbol keys, in the order they were created.
// The order of an object's own keys: integer-like keys in ascending order, then other strings in the order they were
// added, then symbols in the order they were added.
const meta = Symbol('meta');
const mixed = { b: 1, 2: 1, a: 1, 1: 1, [meta]: 1, '-1': 1, '01': 1, 1.5: 1 };
const list = (keys) => keys.map(String).join(', ');
console.log('Object.keys: ', list(Object.keys(mixed)));
console.log('Reflect.ownKeys:', list(Reflect.ownKeys(mixed)));
console.log('JSON: ', JSON.stringify(mixed)); // JSON.stringify writes the same order
// Integer-like means an array index: a whole number from 0 to 4294967294, written without a sign, leading zeros or a
// decimal point.
const edges = { 4294967295: 'too big for an index', 4294967294: 'the largest index', 10: 'ten', 9: 'nine' };
console.log(list(Object.keys(edges)));
// Updating a key keeps its place; deleting it and adding it again moves it to the end.
const steps = { first: 1, second: 2, third: 3 };
steps.first = 10;
console.log(list(Object.keys(steps)));
delete steps.first;
steps.first = 1;
console.log(list(Object.keys(steps))); Output
Object.keys: 1, 2, b, a, -1, 01, 1.5
Reflect.ownKeys: 1, 2, b, a, -1, 01, 1.5, Symbol(meta)
JSON: {"1":1,"2":1,"b":1,"a":1,"-1":1,"01":1,"1.5":1}
9, 10, 4294967294, 4294967295
first, second, third
second, third, first
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node key-order.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 three groups that decide the order of an object's own keys
Text description of the diagram
The diagram starts from the example object, written with the keys `b`, `2`, `a`, `1`, a symbol called `meta`, `'-1'`, `'01'` and `1.5`, in that order, and shows the order JavaScript lists them in.
- First come the integer-like keys, the array indexes, smallest first: `'1'`, then `'2'`.
- Then come all other string keys in the order they were added: `'b'`, `'a'`, `'-1'`, `'01'` and `'1.5'`. Negative numbers, leading zeros and decimals are not integer-like, so they stay in this group.
- Last come symbol keys in the order they were added: `Symbol(meta)`.
`Object.keys`, `Object.entries`, `Object.values`, `JSON.stringify` and `for...in` list the own keys in groups 1 and 2 only; `Reflect.ownKeys` lists all three groups.
An array index is a key that reads exactly like a whole number from 0 to 4,294,967,294 (2 to the power 32, minus 2). Being number-like is not enough:
'1'and'2'are indexes, so they jump ahead of'b'and'a'although they were written later.'-1'has a sign,'01'a leading zero and'1.5'a decimal point, so all three are ordinary strings and stay in the order they were written.4294967295is one past the largest index, so it is an ordinary string key too and comes after the indexes 9, 10 and 4294967294.
Updating an existing key keeps its place, but deleting a key and adding it again creates a new property, which goes
to the end of the string keys. Object.keys, Object.values, Object.entries and JSON.stringify all follow this
order, and so does for...in for an object’s own keys in ordinary cases. Reflect.ownKeys adds the symbols at the
end.
Numeric ids: when the order bites
The rule is harmless for keys such as names, and it causes a real bug when the keys are numbers that carry meaning, such as order ids sent newest first:
// An API sends three orders keyed by id, newest first. As an object, the ids come back in numeric order instead.
const byId = JSON.parse('{"1042": "Asha", "987": "Ravi", "1001": "Meera"}');
console.log(Object.keys(byId));
console.log(Object.values(byId)); // the newest-first order is gone
// Keys that are not integer-like keep the order they arrived in.
const prefixed = JSON.parse('{"o1042": "Asha", "o987": "Ravi", "o1001": "Meera"}');
console.log(Object.keys(prefixed));
// When order matters, send an array; to look orders up by id afterwards, build a Map, which keeps insertion order.
const orders = JSON.parse('[{"id": 1042, "name": "Asha"}, {"id": 987, "name": "Ravi"}, {"id": 1001, "name": "Meera"}]');
console.log(orders.map((order) => order.id));
const nameById = new Map(orders.map((order) => [order.id, order.name]));
console.log([...nameById.keys()], nameById.get(987)); Output
[ '987', '1001', '1042' ] [ 'Ravi', 'Meera', 'Asha' ] [ 'o1042', 'o987', 'o1001' ] [ 1042, 987, 1001 ] [ 1042, 987, 1001 ] Ravi
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node ids-order.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 server sent 1042, 987 and 1001, newest first. Parsed into an object, they come back as 987, 1001, 1042, so a page
that lists Object.values(byId) shows the oldest order first, and no code anywhere sorted anything. The fixes, from
best to worst:
- Send an array when order matters. Arrays keep their order by definition, and each order can carry its id.
- Build a
Mapwhen you need lookups by id as well. AMapkeeps insertion order for every kind of key, numbers included, asnameById.keys()shows. - Prefix the keys, such as
"o1042", so they are not integer-like. It works, but every reader must know the trick.
The other key lists
Object.keys is one of five ways to list keys, and they differ in which keys they include:
// Five ways to list an object's keys, on one object with an inherited, a hidden and a symbol key.
const base = { inherited: true };
const item = Object.create(base);
item.name = 'pen';
item[Symbol('sku')] = 'P-1';
Object.defineProperty(item, 'internal', { value: 42, enumerable: false }); // a non-enumerable property
console.log('Object.keys ', Object.keys(item));
console.log('Object.getOwnPropertyNames', Object.getOwnPropertyNames(item));
console.log('Object.getOwnPropertySymbols', Object.getOwnPropertySymbols(item));
console.log('Reflect.ownKeys ', Reflect.ownKeys(item));
const inLoop = [];
for (const key in item) inLoop.push(key);
console.log('for...in ', inLoop); Output
Object.keys [ 'name' ] Object.getOwnPropertyNames [ 'name', 'internal' ] Object.getOwnPropertySymbols [ Symbol(sku) ] Reflect.ownKeys [ 'name', 'internal', Symbol(sku) ] for...in [ 'name', 'inherited' ]
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node all-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
| Criterion | Object.keys / values / entries | Object.getOwnPropertyNames | Object.getOwnPropertySymbols | Reflect.ownKeys | for...in |
|---|---|---|---|---|---|
| Own string keys | Enumerable ones | All | None | All | Enumerable ones |
| Non-enumerable keys | No | Yes | Yes, for symbols | Yes | No |
| Symbol keys | No | No | Yes | Yes | No |
| Inherited keys | No | No | No | No | Enumerable ones |
| When to choose | Working with an object's data | Inspecting hidden properties | Reading symbol keys | Every own key, for copying or debugging | Rarely: it walks the prototype chain |
Exercise: mapValues and pickBy
Exercise · Easy · JavaScript
Write mapValues and pickBy for plain objects
Write two helpers in object-tools.mjs that work on an object's own enumerable string keys, the keys Object.entries lists:
mapValues(obj, fn)returns a new object with the same keys, where each value isfn(value, key):mapValues({ dosa: 80, idli: 40 }, (price) => price * 2)gives{ dosa: 160, idli: 80 }.pickBy(obj, predicate)returns a new object with only the entries for whichpredicate(value, key)is truthy. With no predicate, it keeps every entry whose value is notundefined, which is handy for dropping missing fields before sending JSON.
Both leave obj unchanged, ignore inherited and symbol keys, keep the usual key order, and treat every key the same, __proto__ included. The versions in the file use for...in and change their input; the tests show what goes wrong.
Starter code · object-tools.mjs
// mapValues and pickBy for plain objects (the exercise of lesson javascript/object-iteration-and-key-order).
// These versions use for...in and change their input. Make them follow the rules of the prompt.
export function mapValues(obj, fn) {
const result = {};
for (const key in obj) result[key] = fn(obj[key], key);
return result;
}
export function pickBy(obj, predicate) {
for (const key in obj) {
if (!predicate(obj[key], key)) delete obj[key];
}
return obj;
} The sample tests · object-tools.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { mapValues, pickBy } from './object-tools.mjs';
test('mapValues applies the function to every value and passes the key', () => {
assert.deepEqual(mapValues({ dosa: 80, idli: 40 }, (price) => price * 2), { dosa: 160, idli: 80 });
assert.deepEqual(mapValues({ size: 'large' }, (value, key) => `${key}=${value}`), { size: 'size=large' });
});
test('pickBy keeps the entries the predicate accepts', () => {
const menu = { dosa: 80, idli: 40, vada: 35 };
assert.deepEqual(pickBy(menu, (price) => price < 50), { idli: 40, vada: 35 });
assert.deepEqual(pickBy(menu, (price, dish) => dish.startsWith('d')), { dosa: 80 });
});
test('pickBy without a predicate drops only undefined values', () => {
const form = { name: 'Asha', phone: undefined, coupon: null, notes: '' };
assert.deepEqual(Object.entries(pickBy(form)), [
['name', 'Asha'],
['coupon', null],
['notes', ''],
]);
});
test('neither helper changes its input, and both return a new object', () => {
const menu = { dosa: 80, idli: 40 };
const mapped = mapValues(menu, (price) => price + 1);
const picked = pickBy(menu, () => false);
assert.deepEqual(menu, { dosa: 80, idli: 40 });
assert.notEqual(mapped, menu);
assert.notEqual(picked, menu);
});
test('inherited and symbol keys are ignored', () => {
const special = Object.create({ inherited: 1 });
special.own = 2;
special[Symbol('note')] = 3;
assert.deepEqual(Reflect.ownKeys(mapValues(special, (value) => value * 10)), ['own']);
assert.deepEqual(Reflect.ownKeys(pickBy(special, () => true)), ['own']);
});
test('number-like keys keep their order and __proto__ is an ordinary key', () => {
const byId = { 10: 'b', 2: 'a', name: 'c' };
assert.deepEqual(Object.entries(mapValues(byId, (s) => s.toUpperCase())), [
['2', 'A'],
['10', 'B'],
['name', 'C'],
]);
const odd = JSON.parse('{"__proto__": 1, "tea": 2}');
assert.deepEqual(Object.entries(mapValues(odd, (n) => n * 10)), [
['__proto__', 10],
['tea', 20],
]);
}); A hint
Object.entries(obj) gives exactly the keys you need, own, enumerable and strings, as [key, value] pairs. Change the pairs with map or filter, then rebuild an object with Object.fromEntries. fromEntries defines each key as an ordinary property, so even '__proto__' becomes a key, which plain assignment such as result[key] = value would not do.
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.
Self-check
Check yourself
6 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 · In what order does Object.keys return an object’s keys?
The order is fixed by the specification, not by chance. First come the keys that look like array indexes, whole
numbers from 0 up to 4,294,967,294 written without a sign, leading zeros or a decimal point, in ascending numeric
order. Then come all other string keys in the order they were added to the object. So { b: 1, 2: 1, a: 1, 1: 1 }
gives ['1', '2', 'b', 'a']. Symbol keys come last, in the order they were added, but Object.keys leaves them out;
Reflect.ownKeys includes them. Updating a key keeps its place, while deleting it and adding it again moves it to the
end. Object.values, Object.entries and JSON.stringify use the same order, which is why numeric ids in an object
come back sorted: use an array or a Map when insertion order matters.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
Two services sign the same order by hashing JSON.stringify(order) with SHA-256. After one service is refactored, the signatures stop matching although both orders hold equal data. A teammate proposes sorting the keys into a new object first. Explain the mismatch, why the proposal still fails for some keys, and what to do.
-
Fresher to mid
To rank players, a function inverts { asha: 980, ravi: 1200, meera: 1100 } into an object keyed by score, then reads Object.values() of it as the ranking. The ranking comes out lowest score first, and when two players tie, one of them disappears. Explain both bugs and write a correct ranking.
Key takeaways
Object.keys,Object.valuesandObject.entriesturn an object’s own, enumerable, string keys into arrays; inherited and symbol keys are left out.Object.fromEntries(pairs)builds an object from any iterable of[key, value]pairs: entries, aMaporURLSearchParams. Together withmapandfilterit transforms objects without changing the original.- Own keys come out in a fixed order: array-index keys in numeric order, then other strings in creation order, then
symbols.
'01','-1'and'1.5'are not indexes. - Updating a key keeps its place; deleting and re-adding it moves it to the end.
- Objects keyed by numeric ids lose their insertion order. Send arrays, or use a
Map, when order matters.
References
- ECMAScript 2026 Language Specification: OrdinaryOwnPropertyKeys (Ecma International)
- ECMAScript 2026 Language Specification: the Object type (array index) (Ecma International)
- ECMAScript 2026 Language Specification: EnumerableOwnProperties (Ecma International)
- ECMAScript 2026 Language Specification: Object.entries (Ecma International)
- ECMAScript 2026 Language Specification: Object.fromEntries (Ecma International)
- ECMAScript 2026 Language Specification: Reflect.ownKeys (Ecma International)
- ECMAScript 2026 Language Specification: EnumerateObjectProperties (for-in order) (Ecma International)
- Object.entries() (MDN Web Docs)
- Object.fromEntries() (MDN Web Docs)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress