skip to content

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

level: seniorimportance: should knowfreq 35%

answer

  1. reads are cheap, writes are not
  2. engines assume the object never changes shape
  3. caches guarded on prototype identity
  4. the penalty spreads to other call sites
  5. set it at creation, not afterwards

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.

solid answer

~50 s

Engines optimise property access by tracking each object's internal shape — which includes its prototype — and caching lookup results at each call site on the assumption that the shape is stable. `Object.setPrototypeOf` (and the equivalent `__proto__` assignment) breaks that assumption after the fact: the affected object drops to a slower path, and call sites that used to see one shape now see two, so previously optimised code can be discarded. Engine and reference documentation warn about this explicitly. Reading with `Object.getPrototypeOf` is cheap; it is the write that costs. The fix is structural — build the object with the right prototype from the start, via `new`, a class, or `Object.create` — so its shape never changes. The narrow legitimate uses, such as repairing an object handed to you by foreign code, belong in one isolated place rather than in hot paths.

code

javascript · 12 lines
javascript
const base = { greet() { return 'hi ' + this.name; } };

// discouraged: shape mutated after creation
const slow = { name: 'a' };
Object.setPrototypeOf(slow, base);

// preferred: correct prototype from birth
const fast = Object.create(base);
fast.name = 'b';

console.log(slow.greet(), fast.greet());                 // 'hi a' 'hi b'
console.log(Object.setPrototypeOf(fast, base) === fast); // true — returns the object

go deeper

for a junior

Know that changing an object's prototype after it exists is a slow operation and that the normal way to give an object a prototype is to create it with one.

for a middle

Explain the mechanism — engines track a shape that includes the prototype and cache lookups against it, so mutation invalidates those caches — and name the creation-time alternatives.

for a senior

Demonstrate production judgment: recognise the deoptimisation spreading to shared call sites, know the throwing cases, and treat mid-life prototype mutation as a construction-path defect to fix at the source.

for a principal

Own the boundary policy — where foreign objects are adapted, whether that adaptation happens once at an ingress point or ad hoc across the codebase, and what the team's rule is for mutation versus rebuilding an object correctly.

## What the engine is doing while you are not looking Modern JavaScript engines make property access fast by assuming objects are stable. Internally each object is associated with a description of its layout — commonly called a shape, map or hidden class — that records which properties it has, in what order, and crucially **what its prototype is**. Objects created the same way share one shape, so the engine can compile a property read into something close to "load the value at a fixed offset" and cache that decision at the call site (an inline cache). When a site has only ever seen one shape, it takes the fastest possible path. Method lookups depend on the prototype for the same reason. `d.speak()` is resolved by walking to `Dog.prototype` once; the engine then caches both the shape check and the found method, and guards that cache so it is only valid while the shape — prototype included — is unchanged. ## Why mutating the prototype is expensive `Object.setPrototypeOf(obj, other)` violates exactly that invariant. The object's shape must change, cached assumptions guarded on the old shape become invalid, and code already optimised on those assumptions can be thrown away and recompiled. The effect is also not confined to the one object: sites that used to see a single shape now see two, so they degrade from the fast monomorphic path to a slower polymorphic or fully generic one — and that penalty applies to objects you never touched, since the call site is shared. Engine and reference documentation both warn in unusually strong terms that changing an object's prototype is a slow operation in every implementation. The cost is asymmetric. `Object.getPrototypeOf(obj)` is an ordinary cheap read. It is the write, in either spelling, that hurts: ```js const a = { x: 1 }; Object.getPrototypeOf(a); // cheap Object.setPrototypeOf(a, proto); // expensive — shape mutation a.__proto__ = proto; // same cost, same mechanism ``` ## Do it at creation instead The structural fix is to give the object its prototype when it is born, so the shape is correct from the first assignment onward: ```js // instead of: const o = {}; Object.setPrototypeOf(o, base); const o = Object.create(base); // or through the constructor path class Base {} class Derived extends Base {} const d = new Derived(); ``` When the shape is right from the start, the engine sees one stable layout, the inline caches stay monomorphic, and nothing has to be invalidated. As a rule of thumb, reaching for `setPrototypeOf` in application code usually means the construction path is wrong — the object is being built somewhere that does not yet know what it is, and the type is patched on afterwards. ## The correctness rules, not just the speed rules The write can also fail outright, which matters if you have it in a code path you cannot let throw: ```js Object.setPrototypeOf({}, null); // fine — null is a valid prototype Object.setPrototypeOf({}, 42); // TypeError — must be an object or null const a = {}; Object.setPrototypeOf(a, a); // TypeError — cycles are rejected const fixed = Object.preventExtensions({}); Object.setPrototypeOf(fixed, { z: 1 }); // TypeError — not extensible Object.setPrototypeOf(fixed, Object.prototype); // allowed — same value, no change ``` It returns the object it was given, so it chains, and it accepts `null` as a target prototype. The non-extensible case has a subtlety worth knowing: setting the *same* prototype an object already has is treated as a no-op and succeeds, while any actual change throws. ## When it is still the right call There are real uses, and they share a shape: one-time repair, off the hot path, at a boundary. Fixing up an object produced by code you do not control so it inherits your methods; a compatibility shim adapting a foreign object to an expected interface; a test helper. The discipline is to confine it to a single well-named function executed once per object at construction time, never inside a loop, a render path, or anything called per request. If profiling shows a hot function where objects change prototype mid-life, that is a design problem to fix at the source rather than optimise around. ## How to answer this in an interview Say the mechanism, not just "it is slow": shapes and inline caches assume a stable prototype, mutation invalidates them and can deoptimise unrelated call sites, and the remedy is to create objects with the prototype they need. That is the difference between repeating advice and explaining it.

  • Is `obj.__proto__ = other` any cheaper than `Object.setPrototypeOf(obj, other)`?
    No. Both end in the same internal operation that mutates the object's prototype link, so both invalidate the engine's shape assumptions and the caches guarded on them. The only differences are stylistic and structural: `__proto__` is a legacy inherited accessor that is missing on objects outside `Object.prototype`'s chain, while `Object.setPrototypeOf` is a core-spec function that always works. Neither spelling avoids the cost.
  • Under what conditions does `Object.setPrototypeOf` throw?
    When the new prototype is neither an object nor `null`; when the assignment would create a cycle, such as making an object its own prototype; and when the object is not extensible and the prototype would actually change. That last case has a wrinkle — setting the same prototype the object already has is a no-op and succeeds even on a non-extensible object, because nothing is really changing.
  • You profile a hot path and find prototype mutation inside it. How do you approach the fix?
    Treat it as a construction defect rather than a micro-optimisation. Find where the object is created and give it the right prototype there — a constructor, a class, or `Object.create` with the intended base — so its shape is correct from the first write. If the mutation exists because the type is only known later, move the decision earlier or build two distinct kinds of object instead of morphing one.

saying these in an interview costs you the question

  • Says setPrototypeOf is slow without naming a mechanism
  • Thinks reading the prototype is equally expensive
  • Believes __proto__ assignment avoids the cost
  • Assumes the penalty affects only the mutated object
  • Claims setPrototypeOf silently fails on non-extensible objects

context