In JavaScript, which property keys does a `for...in` loop visit that `Object.keys(obj)` does not return, and how do you guard a `for...in` loop against them?
answer
- one loop climbs, the other does not
- prototype chain participates in the walk
- own versus inherited, enumerable only
- neither construct ever sees symbols
- guard each iteration with an own-key check
basics
~20 sfor...in walks the prototype chain and visits inherited enumerable string keys as well as own ones; Object.keys returns only the object's own enumerable string keys. Guard each iteration with Object.hasOwn(obj, key), or iterate Object.keys instead.
solid answer
~40 s`for...in` enumerates **enumerable string-keyed properties, own and inherited**: it starts at the object and keeps walking up the prototype chain, so anything an ancestor object contributes shows up in the loop. `Object.keys` (and `Object.values`/`Object.entries`) returns only **own** enumerable string keys — no prototype walk. Neither one ever sees symbol keys, and neither sees properties whose `enumerable` flag is false, which is why class methods and an array's `length` stay out of both. The practical bug is a library or old code adding a property to `Object.prototype` or `Array.prototype`: every `for...in` in the codebase silently grows an extra iteration. The standard guard is `if (!Object.hasOwn(obj, key)) continue;` inside the loop (`Object.hasOwn` is ES2022; before it, `Object.prototype.hasOwnProperty.call(obj, key)`). Simpler still: loop over `Object.keys(obj)` and skip the question entirely.
code
javascript · 16 linesconst proto = { shared: 'from proto' };
const obj = Object.create(proto);
obj.own = 'mine';
Object.defineProperty(obj, 'hidden', { value: 'x', enumerable: false });
const seen = [];
for (const k in obj) seen.push(k);
console.log(seen); // ["own", "shared"]
console.log(Object.keys(obj)); // ["own"]
const guarded = [];
for (const k in obj) {
if (!Object.hasOwn(obj, k)) continue;
guarded.push(k);
}
console.log(guarded); // ["own"]go deeper
Be able to say plainly that for...in also visits inherited keys while Object.keys returns only the object's own keys, and that Object.keys is the safer default for iterating a data object.
Explain the mechanics: for...in climbs the prototype chain filtering only on the enumerable flag, visits each name once, and skips symbols. Show the Object.hasOwn guard and say why the .call form was needed before ES2022.
Demonstrate the production angle: how a dependency writing to Object.prototype changes the behaviour of every for...in in a process, and why you would ban for...in over externally-sourced data rather than rely on every author remembering the guard.
Own the policy call — a lint rule forbidding unguarded for...in, an object-shape convention (null-prototype maps, or Map for dynamic keys), and how you keep untrusted JSON from reaching code that enumerates it in the first place.
## The two axes that decide visibility Every property lookup API in JavaScript filters on two independent things: **ownership** (is the property directly on this object, or on something further up its prototype chain?) and **enumerability** (does the property's `enumerable` attribute say it should show up in enumeration?). Almost every surprise in this area is one of those two axes behaving differently from what you assumed. `for...in` filters on enumerability only — it does **not** filter on ownership. `Object.keys` filters on both: own **and** enumerable. That single sentence explains the whole difference. ## What for...in actually does `for...in` starts at the target object, collects its enumerable string-keyed properties, then moves to `Object.getPrototypeOf(obj)` and repeats, all the way to the end of the chain (usually `Object.prototype`, whose own properties are all non-enumerable, which is why plain objects normally look clean). ```js const proto = { shared: 1 }; const obj = Object.create(proto); obj.own = 2; for (const k in obj) console.log(k); // "own", then "shared" console.log(Object.keys(obj)); // ["own"] ``` Two details worth knowing: - **A name is visited at most once.** If the object has its own `shared`, the inherited `shared` is not visited a second time — the loop sees the shadowing one and moves on. - **A non-enumerable own property still shadows.** If `obj` defines `shared` as non-enumerable, the loop visits neither the own one (not enumerable) nor the inherited one (already accounted for). That is a genuinely surprising corner. `for...in` also never visits symbol-keyed properties — symbols are simply not part of this enumeration at all. ## Why this causes real bugs The classic failure is prototype pollution of built-ins. If any dependency does `Object.prototype.helper = fn`, then every `for...in` over every plain object in the process gains a `helper` iteration. Code that builds a payload by copying keys, or that counts entries, or that validates "unknown field" lists, quietly changes behaviour with no local edit. That is exactly why old style guides insisted every `for...in` carry an own-property guard, and why modern code mostly stopped using `for...in` over data objects at all. A second, subtler case: your own prototypes. If you attach defaults with `Object.create(defaults)` or assign methods with `Proto.method = ...` (an ordinary assignment, so **enumerable**), those show up in `for...in` over instances. Class syntax avoids this — methods declared in a class body are non-enumerable — but hand-rolled prototype assignment is not. ## The guards ```js for (const key in obj) { if (!Object.hasOwn(obj, key)) continue; // ES2022 // ... } ``` Before ES2022 the idiom was `Object.prototype.hasOwnProperty.call(obj, key)`. Note the `.call` — writing `obj.hasOwnProperty(key)` is itself fragile: it breaks on objects created with `Object.create(null)` (no prototype, therefore no inherited `hasOwnProperty`) and on any object that happens to have its own `hasOwnProperty` key, which is common for objects built from untrusted JSON. The better default is not to guard but to choose an API that already filters on ownership: `Object.keys(obj)`, `Object.values(obj)`, `Object.entries(obj)`. These return arrays, so you also get `map`/`filter`/`reduce` for free and never have to think about the chain. ## When for...in is still the right tool It is the only built-in construct that walks the chain for you, so it is genuinely useful when you *want* inherited configuration — a defaults object behind an overrides object, for example, where reading every effective key including the inherited defaults is the point. It is also occasionally used for debugging, to see what an object actually exposes. Outside those cases, treat `for...in` over a data object as a smell: it answers a question ("what enumerable string keys are reachable from here?") that is almost never the question you meant to ask ("what fields does this record have?").
- If an object has its own non-enumerable property with the same name as an inherited enumerable one, what does for...in do?It visits neither. `for...in` tracks names it has already accounted for as it climbs, so the own property shadows the inherited one — but the own one is itself skipped because it is non-enumerable. The name simply never appears in the loop, which surprises people who expect shadowing to be invisible to enumeration.
- Why is `obj.hasOwnProperty(key)` considered unsafe as the guard, and what do you write instead?It relies on inheriting the method. An object created with `Object.create(null)` has no prototype, so the call throws; and an object parsed from untrusted JSON can carry its own `hasOwnProperty` key that shadows the real method and returns anything. Use `Object.hasOwn(obj, key)` (ES2022), or `Object.prototype.hasOwnProperty.call(obj, key)` on older baselines.
- Does for...in enumerate symbol-keyed properties?No. `for...in` is defined over string-keyed properties only, so symbol keys are invisible to it regardless of their `enumerable` flag. The same is true of `Object.keys`/`values`/`entries` and of `JSON.stringify`. To read own symbol keys you need `Object.getOwnPropertySymbols` or `Reflect.ownKeys`.
saying these in an interview costs you the question
- Claims for...in only visits the object's own properties
- Says Object.keys walks the prototype chain like for...in
- Believes for...in returns symbol keys too
- Writes obj.hasOwnProperty(key) on a null-prototype object
- Thinks non-enumerable properties still appear in for...in