Ch. 1 · JavaScript

for...in vs Object.keys vs Reflect.ownKeys: Key Order Explained

Which JavaScript APIs list inherited, non-enumerable and symbol keys, and why integer-like keys always come first in object property order.

~6 min readbeginnerupdated Oct 6, 2026

“What is the difference between for...in and Object.keys?” is a common opener in JavaScript interviews, often followed by “In what order will these keys print?” with an object like { b: 1, 2: 'x', a: 2, 1: 'y' }. Many candidates guess insertion order and are surprised that 1 and 2 come first. Interviewers use these questions to check whether you know which keys an API will actually include (own or inherited, enumerable or hidden, string or symbol) and whether you can predict iteration order instead of relying on luck.

This matters in practice: serialising objects, copying them, comparing them, and building UIs from their keys all depend on these rules.

Before you start

You should know what a prototype is (objects inherit properties from another object), what a Symbol is, and that properties can be non-enumerable. If property descriptors are new to you, it is enough to know that enumerable: false hides a property from most listing APIs.

The short answer

for...in loops over enumerable string keys of the object and of its whole prototype chain. Object.keys, Object.values, Object.entries, spread and JSON.stringify use only own enumerable string keys. Object.getOwnPropertyNames adds non-enumerable string keys, Object.getOwnPropertySymbols returns symbol keys, and Reflect.ownKeys returns all own keys of every kind. Every one of them follows the same order: integer-like keys in ascending numeric order, then other string keys in insertion order, then symbols in insertion order.

How it works

Build one object that has every kind of key, then ask each API what it sees.

const base = { inherited: true };
const obj = Object.create(base);
obj.b = 1;
obj[2] = 'two';
obj.a = 2;
obj[1] = 'one';
obj[Symbol('tag')] = 'sym';
Object.defineProperty(obj, 'hidden', { value: 'h', enumerable: false });

const forIn = [];
for (const k in obj) forIn.push(k);
console.log(forIn); // [ '1', '2', 'b', 'a', 'inherited' ]
console.log(Object.keys(obj)); // [ '1', '2', 'b', 'a' ]
console.log(Object.getOwnPropertyNames(obj)); // [ '1', '2', 'b', 'a', 'hidden' ]
console.log(Object.getOwnPropertySymbols(obj)); // [ Symbol(tag) ]
console.log(Reflect.ownKeys(obj)); // [ '1', '2', 'b', 'a', 'hidden', Symbol(tag) ]
console.log(JSON.stringify(obj)); // {"1":"one","2":"two","b":1,"a":2}
JavaScript

The ordering rule comes from the specification’s OrdinaryOwnPropertyKeys operation. “Integer-like” means an array index: a canonical non-negative integer string below 2³² − 1. So '1' and '42' sort first, but '01', '-1' and '1.5' are ordinary strings and keep their insertion position.

const odd = { '01': 'a', 10: 'b', '-1': 'c', 3: 'd', '1.5': 'e' };
console.log(Object.keys(odd)); // [ '3', '10', '01', '-1', '1.5' ]
JavaScript

for...in lists own keys first and then walks up the prototype chain, skipping keys it has already seen (so a shadowed property is listed once) and skipping non-enumerable ones. Built-in methods like toString live on Object.prototype but are non-enumerable, which is why they never show up.

Step-by-step walkthrough

Step 1: Prefer own keys for data objects

When an object represents data, you almost always want its own properties only. Object.entries with for...of gives you keys and values together and never wanders into prototypes.

const scores = { ada: 9, bo: 7 };
for (const [name, score] of Object.entries(scores)) {
  console.log(`${name}: ${score}`);
}
// ada: 9
// bo: 7
JavaScript

Step 2: Guard for…in when you must use it

If you use for...in, filter with Object.hasOwn (or the older Object.prototype.hasOwnProperty.call). Do not call obj.hasOwnProperty directly; the object might not have it (created with Object.create(null)) or might override it.

const defaults = { theme: 'light' };
const prefs = Object.create(defaults);
prefs.lang = 'en';

const own = [];
for (const key in prefs) {
  if (Object.hasOwn(prefs, key)) own.push(key);
}
console.log(own); // [ 'lang' ]

const bare = Object.create(null);
bare.x = 1;
console.log(Object.hasOwn(bare, 'x')); // true
console.log(typeof bare.hasOwnProperty); // undefined
JavaScript

Step 3: Include symbols and hidden keys deliberately

Libraries often store metadata under symbols or non-enumerable keys exactly so that ordinary code does not see it. When you are writing a faithful clone or a debugging tool, use Reflect.ownKeys with descriptors.

const meta = Symbol('meta');
const record = { id: 1, [meta]: { version: 3 } };

console.log({ ...record }[meta]); // { version: 3 }
console.log(JSON.parse(JSON.stringify(record))[meta]); // undefined
console.log(Reflect.ownKeys(record).length); // 2
JavaScript

Object spread copies own enumerable symbol keys, while JSON drops them. Knowing which operations keep symbols avoids surprises when a value round-trips through an API.

Step 4: Do not use for…in on arrays

for...in on an array yields index strings, includes any extra enumerable properties, and walks the prototype if someone added enumerable methods there. Use for...of, forEach, or entries().

const list = ['x', 'y'];
list.label = 'letters';
const viaIn = [];
for (const k in list) viaIn.push(k);
console.log(viaIn); // [ '0', '1', 'label' ]
const viaOf = [];
for (const v of list) viaOf.push(v);
console.log(viaOf); // [ 'x', 'y' ]
JavaScript

Worked scenario

A settings page built its form fields from for...in over a settings object. Another team added default settings by setting up a prototype, and a third-party script added an enumerable helper to Object.prototype. The form suddenly rendered fields for the defaults and for the helper, and saving posted them to the server.

Object.prototype.debugDump = function () {};
const defaults = { autosave: true };
const settings = Object.assign(Object.create(defaults), { theme: 'dark', fontSize: 14 });

function fieldsBroken(s) {
  const out = [];
  for (const k in s) out.push(k);
  return out;
}
console.log(fieldsBroken(settings)); // [ 'theme', 'fontSize', 'autosave', 'debugDump' ]
delete Object.prototype.debugDump;
JavaScript

Switching to Object.keys lists only what the user actually set, and is immune to prototype additions. If defaults must appear, merge them explicitly so the intent is visible.

Object.prototype.debugDump = function () {};
const defaults = { autosave: true };
const userSettings = { theme: 'dark', fontSize: 14 };

const settings = { ...defaults, ...userSettings };
console.log(Object.keys(settings)); // [ 'autosave', 'theme', 'fontSize' ]
delete Object.prototype.debugDump;
JavaScript

Spread only copies own enumerable properties, so the polluted prototype method is not copied either.

Common mistake

  • “Object keys are always in insertion order.” Integer-like keys jump to the front in ascending order.
  • “for...in and Object.keys are the same.” for...in includes inherited enumerable keys.
  • “Object.keys lists everything.” It skips symbols and non-enumerable properties.
  • “Order is undefined, so never rely on it.” Since ES2020, for...in order is also specified for ordinary cases, and Object.keys has had a defined order since ES2015. You can rely on it, but if order is part of your data’s meaning, a Map or an array communicates that better.
  • Using numeric-string ids as object keys and expecting insertion order. { 300: ..., 100: ... } lists 100 first. Use a Map, which keeps true insertion order for every key.

Verify the behavior

import assert from 'node:assert/strict';

const o = { z: 1, 10: 'a', 2: 'b' };
assert.deepEqual(Object.keys(o), ['2', '10', 'z']);

const child = Object.create({ up: 1 });
child.own = 2;
const seen = [];
for (const k in child) seen.push(k);
assert.deepEqual(seen, ['own', 'up']);
assert.deepEqual(Object.keys(child), ['own']);

const s = Symbol('s');
const withSym = { a: 1, [s]: 2 };
assert.equal(Object.keys(withSym).length, 1);
assert.equal(Reflect.ownKeys(withSym).length, 2);
console.log('enumeration rules hold');
// enumeration rules hold
JavaScript

Run it with node enumeration.mjs.

Follow-up questions

Does Object.entries include symbol keys? No. Use Reflect.ownKeys and read values yourself.

Does Object.assign copy symbols? Yes, own enumerable symbol properties are copied, the same as spread.

Which order does Map use? Pure insertion order for all key types, with no integer-key reordering.

How do you list an object’s non-enumerable properties? Object.getOwnPropertyNames(obj) filtered by !Object.getOwnPropertyDescriptor(obj, k).enumerable.

Is enumeration order the same for arrays? Arrays are objects, so Object.keys(['a', 'b']) gives ['0', '1'] in index order, followed by any extra string properties. Iterating values with for...of uses the array iterator instead, which reads indices from 0 to length - 1 and ignores extra properties entirely.

What happens if you add a key while looping with for...in? The specification does not guarantee whether keys added during the loop are visited, while deleted keys that have not been visited yet are skipped. Collect the keys first with Object.keys if you need to modify the object inside the loop.

Interview exercise

Predict the output, then explain each line.

const proto = { inherited: 1 };
const obj = Object.create(proto);
obj.b = 1;
obj['10'] = 2;
obj.a = 3;
obj['9'] = 4;
obj[Symbol.for('s')] = 5;
const keys = [];
for (const k in obj) keys.push(k);
console.log(keys.join(','));
console.log(Object.keys(obj).join(','));
console.log(Reflect.ownKeys(obj).length);
// 9,10,b,a,inherited
// 9,10,b,a
// 5
JavaScript

Answer and reasoning

The integer-like keys '9' and '10' come first, in numeric (not string) order, which is why 9 precedes 10. Then the string keys b and a follow in insertion order. for...in then continues to the prototype and adds inherited; symbols are never visited by for...in. Object.keys shows the same own string keys without the inherited one. Reflect.ownKeys counts every own key, including the symbol: four strings plus one symbol make five. Explaining that '10' sorts after '9' because the comparison is numeric shows you understand the rule rather than memorised an example.

Continue learning

Find more JavaScript interview questions and test yourself with the JavaScript MCQs. Related notes: property descriptors, Map versus object and well-known symbols. Official reference: Enumerability and ownership of properties on MDN.

More in JavaScript

read ✓JavaScript · easy

JavaScript Rest and Spread Gotchas

Where JavaScript's ... syntax surprises people: merge order, undefined overwriting defaults, lost getters and spreads that blow the stack.

~7 min readread →
esc