skip to content

What does `Object.keys({ b: 1, 2: 'two', a: 3, 1: 'one' })` return, and what rule produces that order?

level: middleimportance: should knowfreq 52%

answer

  1. objects are not unordered bags
  2. some keys jump the queue
  3. numeric-looking keys sort ahead
  4. only canonical integer strings qualify
  5. then everything else in insertion order

basics

~10 s

It returns ["1", "2", "b", "a"]. Own keys come back in a fixed order: array-index-like integer strings first in ascending numeric order, then the remaining string keys in the order they were created.

solid answer

~50 s

The result is `["1", "2", "b", "a"]`. JavaScript objects are not "unordered bags" and never have been in practice: own property keys are produced in a specified order — first every key that is an **array index** (a canonical non-negative integer string below 2^32−1) in ascending numeric order, then all remaining **string** keys in insertion order, and finally, for APIs that expose them, **symbol** keys in insertion order. So `1` and `2` jump ahead of `b` even though `b` was written first. Only canonical integer strings get promoted — `'-1'`, `'1.5'`, `'01'` and `'1e2'` are ordinary string keys and stay in insertion order. This bites whenever you key an object by numeric ID and expect the original sequence back; if order matters, use an array of entries or a `Map`, which preserves pure insertion order for every key type.

code

javascript · 10 lines
javascript
const o = { b: 1, 2: 'two', a: 3, 1: 'one' };
console.log(Object.keys(o));      // ["1", "2", "b", "a"]
console.log(JSON.stringify(o));   // {"1":"one","2":"two","b":1,"a":3}

o['-1'] = 'neg';
o['01'] = 'oh-one';
console.log(Object.keys(o));      // ["1","2","b","a","-1","01"]

const m = new Map([['b', 1], [2, 'two'], ['a', 3], [1, 'one']]);
console.log([...m.keys()]);       // ["b", 2, "a", 1]

go deeper

for a junior

Know that keys that look like non-negative integers come back first in numeric order, and that everything else keeps insertion order — enough to recognise why a list keyed by ID renders out of sequence.

for a middle

Explain the three-group rule precisely and state the exact array-index test, including why '-1' and '01' are ordinary string keys. Name which APIs the guarantee covers and that for...in is the exception.

for a senior

Show the diagnosis: an ID-keyed lookup silently re-sorted, tests that pass because the fixture IDs were already ascending, and serialized payloads whose byte order changes. Argue for arrays or Map when order is part of the data.

for a principal

Own the data-modelling stance — ordering that matters belongs in a structure that models it, not in key syntax; and set the rule for wire formats where key order affects hashing, caching or diffing across services.

## The rule The spec's internal `OrdinaryOwnPropertyKeys` operation defines the order of an ordinary object's own keys as three concatenated groups: 1. **Array-index keys**, ascending by numeric value. 2. **All other string keys**, in property-creation (insertion) order. 3. **Symbol keys**, in property-creation order. Every own-key API is built on that operation, so they all agree with each other. ```js const o = { b: 1, 2: 'two', a: 3, 1: 'one' }; Object.keys(o); // ["1", "2", "b", "a"] Reflect.ownKeys(o); // ["1", "2", "b", "a"] JSON.stringify(o); // {"1":"one","2":"two","b":1,"a":3} ``` ## What counts as an "array index" This is where candidates go wrong. It is not "looks numeric" — it is a strict test: the key must be the **canonical decimal string** of a non-negative integer strictly below 2^32−1. Concretely: - `'0'`, `'7'`, `'42'` — array indices, promoted to the front. - `'-1'` — negative, so not an index; ordinary string key. - `'1.5'` — not an integer; ordinary string key. - `'01'` — the number 1's canonical string is `'1'`, not `'01'`; ordinary string key. - `'1e2'` — canonical form is `'100'`; ordinary string key. - `'4294967295'` (2^32−1) — out of the array-index range; ordinary string key. ```js const o = {}; o.z = 1; o[2] = 2; o['-1'] = 3; o['01'] = 4; o[0] = 5; Object.keys(o); // ["0", "2", "z", "-1", "01"] ``` Note that writing `o[2]` and `o['2']` are the same property: non-symbol keys are converted to strings, so an object never really has numeric keys, only numeric-looking string keys. ## Which APIs guarantee it `Object.getOwnPropertyNames` and `Reflect.ownKeys` have followed this order since ES2015. ES2020 extended the same guarantee to `Object.keys`, `Object.values`, `Object.entries` and `JSON.stringify`, which had previously been left implementation-defined even though every engine already behaved this way. `for...in` is the exception. Its ordering is still **not fully specified** — the spec deliberately leaves room because the loop also walks the prototype chain. In practice mainstream engines produce the same integer-first order for the own portion, but you should never write code that depends on it, and in an interview it is worth flagging as the one construct without a guarantee. ## Why this shows up as a bug The common shape is a lookup object keyed by database ID or by year, built in a meaningful sequence and then rendered by iterating its keys: ```js const byId = {}; for (const row of rowsInPriorityOrder) byId[row.id] = row; Object.keys(byId); // sorted ascending by id — the priority order is gone ``` Because every `row.id` is an integer-like string, the entire object is silently re-sorted numerically. Nothing throws; the list just comes out in the wrong order, and it looks correct in tests where the IDs happened to be ascending already. The same thing hits objects deserialized from JSON, and it changes the byte output of `JSON.stringify`, which matters if you hash or diff serialized payloads. ## The fixes If order is part of the data, stop encoding it in object keys. An **array of entries** (`[{ id, ...row }]`) keeps whatever order you put in it. A **`Map`** keeps strict insertion order for all key types and, as a bonus, keeps numeric keys as actual numbers rather than stringifying them. If you must keep a plain object, sort explicitly at the point of use with an explicit comparator, so the ordering is visible in the code instead of being an emergent property of key syntax. One last note: prefixing keys (`'id-' + row.id`) also defeats the promotion, since the key is no longer a canonical integer string. It works, but it hides intent — prefer the data structure that actually models ordering.

  • Where do symbol keys land in that ordering, and which APIs show them?
    Symbols come last, after all string keys, in creation order. Only `Reflect.ownKeys` and `Object.getOwnPropertySymbols` expose them; `Object.keys`, `for...in` and `JSON.stringify` are string-key-only, so symbols are invisible there regardless of their enumerable flag.
  • Is `for...in` ordering guaranteed by the specification?
    No. `Object.keys`, `Object.values`, `Object.entries`, `Object.getOwnPropertyNames`, `Reflect.ownKeys` and `JSON.stringify` all have a specified order, but `for...in` is left partly implementation-defined because it also walks the prototype chain. Engines happen to agree on the own-property portion; treat that as a coincidence, not a contract.
  • Does the key `'01'` get promoted to the front like `'1'` does?
    No. Promotion requires the key to be the canonical decimal string of the integer, and the canonical form of 1 is `'1'`. `'01'`, `'1.5'`, `'-1'` and `'1e2'` are all ordinary string keys and stay in insertion order — which is occasionally used deliberately to stop numeric-looking keys from reordering.

saying these in an interview costs you the question

  • Says object key order is undefined or random
  • Claims insertion order applies to every key type
  • Thinks any numeric-looking key is promoted, including '-1'
  • Assumes for...in has the same specified ordering guarantee
  • Believes JSON.stringify emits keys in literal source order

context