skip to content

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%

answer

  1. reads and writes take different paths
  2. first match wins, then stop
  3. assignment lands on the receiver
  4. missing everywhere means undefined, not an error
  5. delete the own one, inherited reappears

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.

solid answer

~40 s

Reads and writes take different paths. On a read the engine looks for an own property of `obj`; finding none, it follows the object's internal prototype link, then that object's link, and so on until the name is found or the chain ends (normally at `Object.prototype`, whose prototype is `null`). Missing everywhere just evaluates to `undefined` — no error is thrown. A write is different: `obj.x = 1` normally creates a **new own** data property on `obj` itself, so the prototype's `x` is untouched and merely hidden. That is shadowing. The write path does consult the chain, but only to check for an inherited setter or a non-writable inherited property; otherwise the value lands on the receiver. `delete obj.x` removes only the own property, after which the inherited one becomes visible again.

code

javascript · 13 lines
javascript
const proto = { label: 'default' };
const obj = Object.create(proto);

console.log(obj.label);                    // 'default' (inherited)
console.log(Object.hasOwn(obj, 'label'));  // false

obj.label = 'mine';                        // creates an own property
console.log(obj.label);                    // 'mine'
console.log(proto.label);                  // 'default' — untouched
console.log(Object.hasOwn(obj, 'label'));  // true

delete obj.label;                          // removes only the own one
console.log(obj.label);                    // 'default' again

go deeper

for a junior

Know that an object can use properties it does not itself have, that a missing property reads as undefined instead of throwing, and that writing to a name puts the value on that object.

for a middle

Explain the two paths concretely: [[Get]] walks prototype links until the first match or null, while [[Set]] creates an own property on the receiver. Be able to show shadowing and undo it with delete.

for a senior

Show where the write path does consult the chain — inherited setters and non-writable properties — and how the sloppy-versus-strict silent failure turns into a real debugging session. Tie it to bugs from state placed on prototypes.

for a principal

Frame it as an API design constraint: the chain gives cheap shared behaviour but no isolation of state, and a chain that is deep or mutated at run time trades predictability and engine optimisation for flexibility. Say when a plain map or a null-prototype object is the better container.

## The mental model Every JavaScript object holds an internal link to another object or to `null` — its **[[Prototype]]**, readable with `Object.getPrototypeOf(obj)`. That link is a live reference, not a copy: nothing is duplicated into the object when it is created. Property access is therefore resolved dynamically, on every access, by walking that chain of links. The single most useful fact about the chain is that **reads walk it and writes normally do not**. ## The read path `obj.x` performs the internal [[Get]] operation. The engine asks whether `obj` has an own property named `x`. If yes, it uses it and stops. If not, it moves to `Object.getPrototypeOf(obj)` and repeats. The walk ends either at the first object that owns the name — the first match wins, deeper links are never consulted — or when the chain reaches `null`. If the walk ends without a match, the result is `undefined`. This is a common trip-up: an absent property is not an error, so a typo in a property name is silent. (An undeclared *variable* is different — that throws a `ReferenceError` — but a missing property never does.) ```js const proto = { greet() { return 'hi'; } }; const obj = Object.create(proto); obj.greet(); // 'hi' — found one link up obj.nothing; // undefined — walk hit null ``` One subtlety: if the property found on a prototype is an accessor, its getter runs with `this` bound to the object the access *started* from, not the prototype that owns the getter. That is what makes shared methods work at all — a method living on `Ctor.prototype` sees the instance as `this`. ## The write path `obj.x = 1` performs [[Set]] with `obj` as the **receiver**. In the ordinary case, the engine creates an own data property on the receiver, with `writable`, `enumerable` and `configurable` all true. The prototype is never modified. If `obj` already has its own `x`, only that own value is updated. ```js const proto = { count: 0 }; const a = Object.create(proto); const b = Object.create(proto); a.count = 5; a.count; // 5 — own property b.count; // 0 — still reads the prototype proto.count; // 0 — untouched Object.hasOwn(a, 'count'); // true Object.hasOwn(b, 'count'); // false ``` This is **shadowing**: `a` now owns a `count` that hides the inherited one. Since the own property is created fresh, it does not inherit the prototype property's attributes. ## Where the write path does consult the chain The write is not entirely blind to the prototype. Before creating the own property, [[Set]] walks the chain looking for an existing property of that name, and three cases divert it: - The chain has an **accessor with a setter** — the setter is called with `this` set to the receiver, and no own property is created. - The chain has an **accessor with only a getter** — the assignment fails: silently in sloppy mode, with a `TypeError` in strict mode (which includes all module and class code). - The chain has a **non-writable data property** — same outcome: ignored in sloppy mode, `TypeError` in strict mode. `Object.defineProperty(obj, 'x', { value: 1 })` bypasses all of that; it always defines directly on the target and never triggers an inherited setter. ## Un-shadowing with delete Because the own property and the inherited one are separate, removing the own property reveals the prototype's again: ```js delete a.count; // removes only a's own property a.count; // 0 — the inherited value is visible again ``` `delete` never removes an inherited property; to change that you must delete it from the object that actually owns it. ## Why it matters day to day The read/write asymmetry explains the classic bug where mutable state placed on a prototype looks shared for some operations and private for others: mutating through the inherited reference touches the one shared object, while assigning to the name creates a private copy on the instance. It also explains why checking membership needs the right tool — `'x' in obj` is true for inherited names, while `Object.hasOwn(obj, 'x')` is true only for own ones — and why data objects coming from outside the program should be checked with own-property tests rather than plain truthiness. Per-instance state therefore belongs on the instance: assigned in the constructor, or declared as a class field, which is defined on each instance rather than on the prototype. Shared *behaviour* — methods — belongs on the prototype, where sharing is exactly what you want.

  • When a getter found on a prototype runs, what is `this` inside it?
    `this` is the object the access started from — the receiver — not the prototype that owns the getter. That is what lets one accessor defined once on a prototype read per-instance state from many different instances. The same rule applies to ordinary methods found on the chain.
  • Does reading an inherited property ever copy it onto the instance?
    No. The read leaves the object untouched; the value is fetched from wherever the walk found it, every time. That is why changing the prototype's value is immediately visible to every object that has not shadowed the name, and why `Object.hasOwn` still reports false after many reads.
  • How does `Object.defineProperty(obj, 'x', { value: 1 })` differ from `obj.x = 1` here?
    `defineProperty` operates directly on the target: it never walks the chain, never triggers an inherited setter, and is not blocked by a non-writable inherited property. It also defaults the unspecified attributes to false, so the resulting own property is non-writable, non-enumerable and non-configurable unless you say otherwise.

saying these in an interview costs you the question

  • Says assignment updates the property on the prototype
  • Thinks reading a missing property throws an error
  • Believes a read copies the inherited value onto the object
  • Claims delete on the instance removes the prototype's property
  • Says the walk keeps going and merges values from every level

context