“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}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' ]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: 7Step 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); // undefinedStep 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); // 2Object 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' ]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;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;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...inandObject.keysare the same.”for...inincludes inherited enumerable keys. - “
Object.keyslists everything.” It skips symbols and non-enumerable properties. - “Order is undefined, so never rely on it.” Since ES2020,
for...inorder is also specified for ordinary cases, andObject.keyshas had a defined order since ES2015. You can rely on it, but if order is part of your data’s meaning, aMapor an array communicates that better. - Using numeric-string ids as object keys and expecting insertion order.
{ 300: ..., 100: ... }lists100first. Use aMap, 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 holdRun 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
// 5Answer 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.