skip to content

In a delegation-based (prototype) object model, describe what happens when you read a name the object does not have, and what happens when you assign to that same name. Contrast that with lookup in a class-based language, naming specific languages.

level: middleimportance: should knowfreq 46%

answer

  1. reads walk the chain, writes shadow on the receiver
  2. prototype primitives privatise on write; prototype arrays stay shared
  3. Lua __newindex can redirect a write; __index can be a function
  4. Self/Io: multiple parent slots, receiver stays self
  5. setPrototypeOf on a live object de-optimises shape caches

basics

~20 s

Reads walk the delegation chain until a holder is found; writes normally do not walk — they create an own property on the receiver that shadows the parent's. So prototype state looks shared until the first assignment, while mutating a prototype's array or object stays shared for everyone.

solid answer

~60 s

- **JavaScript**: `obj.x` walks the `[[Prototype]]` chain; `obj.x = v` defines an *own* property on `obj`, so the prototype's value is shadowed, not updated (unless the chain holds a setter or a non-writable property, which throws in strict mode). Hence the classic asymmetry: `this.count++` privatises the counter, while `this.items.push(1)` on a prototype array mutates the one copy everybody shares. The chain is live: adding a method to a prototype changes objects that already exist. Cost: engines specialise on object shape, and `Object.setPrototypeOf` on a live object de-optimises it. - **Lua**: writes are interceptable. `__newindex` can redirect an assignment to the parent table or reject it, so Lua can do what JavaScript cannot — make a write not shadow. `__index` may also be a *function*, i.e. computed lookup rather than a table walk. - **Self / Io**: several parent slots per object, and `self` stays the original receiver through delegation — that is the difference from copying. - **Python** is class-based but still retroactive: the MRO is linearised once by C3 at class creation, yet monkeypatching a class is visible to existing instances. - **C++, Rust, Go**: resolved when compiling; there is no chain to modify at all.

code

javascript · 9 lines
javascript
const proto = { count: 0, items: [] };
const a = Object.create(proto);
const b = Object.create(proto);

a.count++;        // own property on a
console.log(b.count);       // 0  -- shadowed, not shared

a.items.push('x');          // no assignment: mutates proto.items
console.log(b.items.length); // 1  -- still shared

go deeper

for a junior

Be able to state the asymmetry: reads look up the chain, writes land on the object you wrote to.

for a middle

Add the mutable-shared-value exception and know the chain is live so later prototype changes affect existing objects.

for a senior

Contrast with a language that makes writes programmable, such as Lua's __newindex, and mention the shape/inline-cache cost of rewiring prototypes at run time.

for a principal

Frame the choice as where the behaviour holder lives and how mutable it is, and note that class-based languages such as Python and Ruby share the retroactivity while keeping a single per-type spine that tooling can reason about.

## Two ways to answer 'where does this behaviour live?' A class-based language answers with a fixed structure decided before instances exist: an instance carries a pointer to its class, the class to its superclass, and lookup is a walk over that fixed spine. A prototype-based language answers with an ordinary object: every object has a link to another object, and lookup is a walk over links you can read, write and replace at run time. The mechanics of *read* versus *write* is where the model's character actually shows. ## Reads delegate In JavaScript, evaluating `obj.render` checks `obj`'s own properties, then its `[[Prototype]]`, then that object's prototype, up to `Object.prototype` and finally `null`. Nothing is copied; the receiver stays `obj`, so `this` inside the found function still refers to the original object. That is the essence of delegation as opposed to cloning: behaviour is borrowed, not duplicated. Self and Io generalise it to multiple parent slots, so an object can delegate to several parents at once — a per-object multiple inheritance with the same ambiguity problems classes have, resolved by slot ordering. ## Writes usually do not Assignment is asymmetric in JavaScript: `obj.count = 1` creates an own property on `obj`. The prototype is untouched, and from now on `obj` has its own value that hides the inherited one. Two consequences dominate real bugs. First, shared-looking primitives silently privatise on the first write, so a counter on the prototype is not a shared counter. Second, shared *mutable* values do not: `obj.items.push(x)` never assigns to `obj`, it reads `items` through the chain and mutates the single array on the prototype, which every instance sees. This is why the guidance is to put methods on the prototype and per-instance data in the constructor. The rule has exceptions that surprise people: if the chain holds an accessor property, the setter runs instead of shadowing; if it holds a non-writable data property, the assignment is a silent no-op in sloppy mode and a `TypeError` in strict mode and inside class bodies. ## Lua shows the design space is wider Lua's metatables make both halves programmable. `__index` is consulted on a failed read and may be a table (delegate to it) or a function (compute the answer), which subsumes both delegation and computed properties. `__newindex` is consulted on a write to a *missing* key, so a Lua object can forward assignments to its parent, log them, or reject them outright — precisely the behaviour JavaScript hardcodes as 'always shadow'. Comparing the two makes clear that write-shadowing is a language decision, not a property of prototypes. ## Class-based, but still mutable Python is class-based: attribute lookup consults the instance dict, then the type's MRO, which was linearised by the C3 algorithm once when the class was created. So Python does not walk a live per-object link the way JavaScript does. And yet monkeypatching a Python class is visible to instances that already exist, because instances hold a reference to the class object and the class's `__dict__` is mutable. Ruby goes further and reopens classes as a first-class idiom. So 'retroactive change' is not the prototype/class dividing line — mutability of the behaviour holder is. The real dividing line is whether each *object* can have its own parent link. ## Where the chain does not exist C++, Rust and Go resolve member and trait lookup when compiling. There is nothing to modify at run time; 'add a method to all existing objects' is not expressible, and extension goes through new free functions or trait implementations, with Rust's orphan rule limiting even that. What they gain is that lookup costs nothing at run time and tooling is exact. ## The cost side in JavaScript Engines optimise property access by assuming stable object shapes and caching lookups per call site. Objects built the same way share a hidden class, and the cache hits. Reassigning a prototype on a live object (`Object.setPrototypeOf`) invalidates that assumption and typically drops the object into a slow mode, which is why the advice is to establish the chain at creation time (`Object.create(proto)` or a constructor/`class`) rather than rewiring later.

  • How does Object.create(proto) differ from calling a constructor with new?
    Object.create makes a fresh object whose prototype link is exactly the object you passed, and runs no initialisation. new Ctor() allocates an object whose prototype is Ctor.prototype, runs the constructor body with this bound to it, and uses the constructor's return only if it is an object. Object.create is the primitive; new is the packaged version that also initialises per-instance state.
  • Why is a mutable object placed on a prototype a recurring source of bugs, and what is the idiomatic fix?
    Because reading it goes through the chain to the single shared instance, and mutating it in place never assigns, so no shadowing copy is ever made and every delegating object observes the change. The fix is to create per-object state during construction — assign the array or object inside the constructor or class field — and keep only functions on the prototype.

saying these in an interview costs you the question

  • Saying assignment updates the prototype; it creates an own property on the receiver instead.
  • Concluding from that rule that prototype state is always safe, missing that in-place mutation of a shared array or object never shadows.
  • Believing retroactive change is unique to prototypes — Python and Ruby classes are mutable at run time too.
  • Treating Object.setPrototypeOf as a free operation rather than a shape de-optimisation.
  • Claiming delegation copies the parent's behaviour into the child; nothing is copied and the receiver stays the original object.

context