skip to content

The Prototype Chain

How JavaScript resolves a property you never defined on the object itself: it walks a chain of linked objects until it finds the key or reaches null. Everything else in this area — new, classes, inheritance — is a convenience built on this one mechanism, so interviewers start here.

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

explore

questions

14

In JavaScript, how do `'toString' in obj` and `Object.hasOwn(obj, 'toString')` differ for a plain object literal, and when does that difference matter?

level: juniorimportance: must knowfreq 58%

answer

  1. one walks the chain, one does not
  2. inherited names are still reachable names
  3. data versus capability
  4. beware objects with no prototype
  5. presence is not the same as a defined value

basics

~20 s

The in operator searches the whole prototype chain, so it reports true for inherited names such as toString. Object.hasOwn checks only the object's own properties and reports false. Use hasOwn whenever you mean data the object itself carries.

solid answer

~50 s

`in` asks whether the name is reachable at all: it checks own properties and then every prototype up the chain, so `'toString' in {}` is `true` because `Object.prototype` supplies it. `Object.hasOwn(obj, 'toString')` asks only whether the object itself defines the property, so for a plain literal it is `false`. The distinction matters whenever you are inspecting data rather than capability — validating a parsed JSON payload, counting fields, or deciding whether a caller passed an option — because inherited names would otherwise produce false positives. `Object.hasOwn` was added in ES2022 as a safer replacement for `obj.hasOwnProperty(key)`, which breaks when the object has no prototype or defines its own `hasOwnProperty`. Note that both are membership checks, not value checks: a property explicitly set to `undefined` still reports `true`, whereas `obj.key !== undefined` cannot tell the two cases apart.

code

javascript · 15 lines
javascript
const obj = { a: 1, missing: undefined };

console.log('a' in obj);                       // true
console.log('toString' in obj);                // true  (inherited)
console.log(Object.hasOwn(obj, 'toString'));   // false (not own)

// presence vs value
console.log(obj.missing !== undefined);        // false
console.log(Object.hasOwn(obj, 'missing'));    // true

// why the method form is unsafe
const dict = Object.create(null);
dict.k = 1;
console.log(Object.hasOwn(dict, 'k'));         // true
// dict.hasOwnProperty('k')                    // TypeError: not a function

go deeper

for a junior

Be able to say that in finds inherited names like toString while Object.hasOwn only reports properties the object itself defines, and reach for hasOwn when inspecting plain data.

for a middle

Explain the mechanics: in runs the same chain walk as a read, hasOwn stops at the object. Cover why presence differs from a truthiness test and why hasOwn replaced calling hasOwnProperty as a method.

for a senior

Show the judgment: inherited names in data checks are a correctness and security seam for externally supplied payloads, and null-prototype objects or Map are the structural fix rather than defensive guards everywhere.

for a principal

Own the convention across a codebase — one own-property helper, data containers that cannot inherit surprises, and a lint rule — so that no individual reviewer has to spot each unsafe membership test.

## Two different questions Property access resolves by walking the prototype chain, so "does this object have `x`" is genuinely ambiguous. JavaScript gives you both readings. **`'x' in obj`** performs the internal [[HasProperty]] operation: own properties first, then each prototype in turn, up to `null`. It answers *is this name reachable*. **`Object.hasOwn(obj, 'x')`** answers *does this object itself define the name*. No walk. ```js const obj = { a: 1 }; 'a' in obj; // true 'toString' in obj; // true — from Object.prototype Object.hasOwn(obj, 'toString'); // false — not its own ``` `Reflect.has(obj, 'x')` is the function form of `in` and gives the same chain-walking answer, so it is not an alternative to `hasOwn`. ## Why hasOwn rather than hasOwnProperty `Object.prototype.hasOwnProperty` does the same own-property test, but calling it as a method has two failure modes. First, an object may not inherit it at all — `Object.create(null)` produces an object with no prototype, so `dict.hasOwnProperty('k')` throws a `TypeError`. Second, an object may define its own property with that name, which is entirely possible when the keys come from parsed JSON: ```js const payload = JSON.parse('{"hasOwnProperty": 1}'); payload.hasOwnProperty('x'); // TypeError: not a function Object.hasOwn(payload, 'x'); // false — works fine ``` The pre-ES2022 workaround was to borrow the method: `Object.prototype.hasOwnProperty.call(obj, key)`. `Object.hasOwn(obj, key)`, added in ES2022, does exactly that with far less ceremony and is the modern default. Both accept string and symbol keys. ## Membership versus value Neither check looks at the value, which is the point of using them instead of a truthiness test: ```js const opts = { retry: undefined }; opts.retry !== undefined; // false — looks absent 'retry' in opts; // true Object.hasOwn(opts, 'retry'); // true — explicitly provided ``` This matters for option objects where "not supplied" and "supplied as `undefined`/`0`/`''`" must be distinguished before applying a default. ## Choosing between them Use `Object.hasOwn` when the object is **data**: a parsed payload, a lookup table, an options bag, a record whose fields you are iterating or counting. Inherited names are noise there, and treating them as data is how a payload key like `constructor` or `toString` sneaks into logic that never expected it. Use `in` when you are asking about **capability**: does this value support a method or a protocol, including one it inherits. ```js if ('then' in value) { /* looks thenable, inherited counts */ } if (Object.hasOwn(config, 'timeout')) { /* the caller supplied it */ } ``` `in` also has a distinct meaning for arrays: it tests index presence rather than range, so it distinguishes a hole from a stored `undefined`. `1 in [1, , 3]` is `false` while `1 in [1, undefined, 3]` is `true`. ## A related trap `in` is also the operator inside a `for...in` loop, and that loop shares the chain-walking behaviour: it visits inherited enumerable string-keyed properties too. That is why code that iterates data objects is usually written with `Object.keys`, `Object.entries` or `for...of` over them, which are own-property based, rather than filtering `for...in` with a `hasOwn` guard after the fact. ## Summary rule Default to `Object.hasOwn` for data and reach for `in` deliberately when inheritance is part of the question you are asking. If you find yourself writing `in` against a payload from outside the program, that is almost always the wrong check.

  • Why do modern style guides prefer `Object.hasOwn(obj, k)` over `obj.hasOwnProperty(k)`?
    Because the method call depends on the object inheriting it. An object built with `Object.create(null)` does not have it and throws, and an object whose data happens to include a `hasOwnProperty` key shadows it with a non-function. `Object.hasOwn`, added in ES2022, performs the same own-property test without touching the object's chain.
  • How would you tell apart an option that was omitted from one explicitly passed as undefined?
    Use `Object.hasOwn(options, 'timeout')`. It reports true for a key present with value `undefined` and false when the key was never set, whereas `options.timeout !== undefined` and destructuring defaults collapse both cases into 'absent' and silently apply the default.
  • Does `Object.hasOwn` work with symbol keys?
    Yes. Own-property checks cover string and symbol keys alike, so `Object.hasOwn(obj, Symbol.iterator)` is a valid test for an own iterator. It returns false for an inherited one, which is common — array instances inherit `Symbol.iterator` from `Array.prototype` rather than owning it.

saying these in an interview costs you the question

  • Says `in` only checks the object's own properties
  • Uses obj.key !== undefined as a presence check
  • Thinks hasOwnProperty walks the prototype chain
  • Assumes every object inherits hasOwnProperty
  • Believes Reflect.has is an own-property check

context

open as a page

In JavaScript, what does the engine do when you read `obj.x` and `x` is not an own property of `obj`, and what does the assignment `obj.x = 1` do instead?

level: middleimportance: must knowfreq 76%

basics

~20 s

A read walks the prototype chain, checking own properties first and then each prototype in turn, and yields undefined if nothing matches. A plain assignment creates an own property directly on obj, shadowing any inherited property of that name rather than updating it.

open as a page

A JavaScript constructor sets `Ctor.prototype.tags = []`. One instance runs `a.tags.push('x')` and another runs `b.tags = ['y']`. Explain why the two operations have completely different effects.

level: middleimportance: must knowfreq 58%

basics

~20 s

Both instances inherit one array object from the prototype, so push mutates that single shared array and every instance sees the change. Assignment does not touch the prototype: it creates an own tags property on that one instance, which then stops sharing.

open as a page

In JavaScript, given a `base` object that holds shared methods, what is the difference between creating a new object with `Object.create(base)` and creating one with `{ ...base }`?

level: juniorimportance: should knowfreq 60%

basics

~20 s

Object.create(base) returns an empty object linked to base, so every read falls through to base and later edits to base are visible. { ...base } copies base's own enumerable properties once into an unlinked object.

open as a page

In JavaScript, why would you build a string-keyed lookup table with `Object.create(null)` instead of `{}`, and what stops working on the resulting object?

level: middleimportance: should knowfreq 50%

basics

~20 s

Object.create(null) makes an object with no prototype, so no key can collide with an inherited member such as toString or constructor and every property present is genuinely one you put there. The cost is that the object has no inherited methods at all.

open as a page

Starting from the array literal `[]`, which objects does its prototype chain link to, and what terminates the chain?

level: middleimportance: should knowfreq 55%

basics

~10 s

An array links to Array.prototype, which links to Object.prototype, which links to null. Null terminates every ordinary prototype chain, so Object.prototype is the last real object a lookup can reach.

open as a page

Why is `__proto__` considered legacy in JavaScript, and which standard APIs replace reading and writing it?

level: middleimportance: should knowfreq 58%

basics

~10 s

proto is a legacy accessor defined on Object.prototype and standardised only in the web-compatibility annex. Object.getPrototypeOf reads an object's prototype link and Object.setPrototypeOf writes it; both are core-language functions that work on any object.

open as a page

Which of JavaScript's callable forms carry a `.prototype` property — function declarations, arrow functions, object-literal method shorthand, classes, and generator functions?

level: middleimportance: should knowfreq 40%

basics

~10 s

Function declarations and expressions, classes, generator functions and async generator functions have a .prototype property. Arrow functions, method shorthand, async functions and bound functions do not; reading .prototype on them gives undefined.

open as a page

In JavaScript, an assignment such as `obj.total = 5` appears to do nothing — reading `obj.total` afterwards still gives the old value and no error is reported. What conditions on the prototype chain cause this, and how would you confirm which one applies?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Assignment consults the prototype chain first. An inherited accessor with no setter, or an inherited non-writable data property, makes the write fail — silently in sloppy mode, with a TypeError in strict mode. An inherited setter can also absorb the value without storing it.

open as a page

Why does a single assignment such as `Object.prototype.isAdmin = true` make `({}).isAdmin` return true even for objects created much earlier, and what does that imply for code that copies keys from untrusted JSON into an object?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Almost every object links to Object.prototype, and lookups resolve through that link on each read rather than at creation time, so one added property becomes visible everywhere immediately. Code that copies untrusted keys can therefore change behaviour for objects it never touched.

open as a page

In JavaScript, the key `__proto__` behaves differently in an object literal, in the result of `JSON.parse`, and in a bracket assignment. Explain the three cases and what they mean for merging untrusted data into an object.

level: seniorimportance: should knowfreq 40%

basics

~20 s

In an object literal __proto__: v sets the new object's prototype instead of creating a property. JSON.parse creates an ordinary own property with that name. A bracket assignment on a normal object runs the inherited proto setter and reassigns the prototype, which is how naive merges reach Object.prototype.

open as a page

Why is calling `Object.setPrototypeOf` on an already-created object discouraged, and what should you do instead?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Changing an existing object's prototype invalidates the engine's optimised assumptions about that object's shape and the caches at every site that touches it, forcing slower generic lookups. Create objects with the prototype they need instead of retrofitting it.

open as a page

What does the optional second argument to `Object.create(proto, props)` expect, and what surprises people about the properties it defines?

level: middleimportance: nice to knowfreq 28%

basics

~20 s

It expects a map of property names to property descriptors, not plain values, and every flag you omit defaults to false — so the properties come out non-writable, non-enumerable and non-configurable unless you say otherwise.

open as a page