Prototypes and the Prototype Chain

4 min read source

Prototypes and the Prototype Chain

TL;DR

Every JS object has a hidden link, [[Prototype]], to another object. Property lookup walks this chain until it finds the key or hits null. Functions have a .prototype object that becomes the [[Prototype]] of instances created with new. class is syntax sugar over this — methods live on Class.prototype, shared by all instances. The senior-level points: __proto__ (the accessor) vs [[Prototype]] (the internal slot), why Object.setPrototypeOf is a performance trap, and why you check Object.hasOwn instead of trusting inherited keys.

Interview Q&A

Q: What is the prototype chain?

A: A linked list of objects. When you read obj.x, the engine checks obj’s own properties, then obj’s prototype, then its prototype, up to Object.prototype, then null.

const animal = { eats: true };
const dog = Object.create(animal);   // dog.[[Prototype]] === animal
dog.barks = true;

dog.barks;  // true  (own)
dog.eats;   // true  (inherited from animal)
dog.toString; // function (inherited from Object.prototype)

Writes, by contrast, almost always create an own property on the object — they don’t modify the prototype (except for inherited setters).

Q: __proto__ vs prototype vs [[Prototype]]?

A:

  • [[Prototype]] — the actual internal slot every object has.
  • __proto__ — a legacy accessor (getter/setter on Object.prototype) exposing that slot. Standardized for the web but discouraged; prefer Object.getPrototypeOf / Object.setPrototypeOf.
  • Constructor.prototype — a property on functions. It’s the object that becomes instance.[[Prototype]] when you call new Constructor(). It is not the function’s own prototype.
function Dog() {}
const d = new Dog();
Object.getPrototypeOf(d) === Dog.prototype;  // true

Q: How does class map onto prototypes?

A: Methods go on .prototype (shared); fields go on the instance (per-object); static members go on the constructor itself.

class Dog {
  legs = 4;            // own, per-instance
  bark() {}            // Dog.prototype.bark — shared
  static species() {}  // Dog.species
}
const a = new Dog(), b = new Dog();
a.bark === b.bark;     // true — same function on the prototype

extends sets Sub.prototype.[[Prototype]] = Base.prototype (instance chain) and Sub.[[Prototype]] = Base (static inheritance).

Q: Why is Object.setPrototypeOf (and __proto__=) a performance trap?

A: Engines optimize property access with hidden classes / inline caches tied to an object’s shape, which includes its prototype. Mutating the prototype of a live object invalidates those caches and can deopt every access site that touched it. Set the prototype at creation (Object.create(proto) or a class) — never mutate it later on hot objects.

Q: How do you check whether a property is own vs inherited?

A:

Object.hasOwn(obj, "x");                 // preferred (ES2022)
Object.prototype.hasOwnProperty.call(obj, "x"); // pre-2022, safe even if obj has its own `hasOwnProperty`
"x" in obj;                              // true for inherited too

for...in iterates inherited enumerable keys; Object.keys returns only own enumerable keys. Prefer Object.keys/entries to avoid surprises.

Q: How do you create an object with no prototype, and why?

A: Object.create(null) — a “dictionary” object with no inherited methods. Used for safe key/value maps so user-supplied keys like "toString" or "__proto__" don’t collide with Object.prototype (a prototype-pollution defense). Downside: no toString, hasOwnProperty, etc. — use Object.hasOwn(obj, k) and Map where possible.

Q: instanceof — how does it work, and when does it lie?

A: x instanceof C walks x’s prototype chain looking for C.prototype. It breaks across realms (an array from an <iframe> is not instanceof the parent’s Array) — use Array.isArray, Object.prototype.toString.call(x), or Symbol.hasInstance overrides knowingly.

Gotchas / edge cases

  • Mutating Array.prototype / Object.prototype breaks the worldfor...in then iterates your addition, libraries collide. Never extend built-in prototypes in app code.
  • Prototype pollutionobj[userKey] = userVal with userKey === "__proto__" can poison Object.prototype. Validate keys, use Object.create(null) or Map for untrusted data. See ../17_security/.
  • Shared mutable state on the prototype — putting an object/array on .prototype shares one instance across all objects: Dog.prototype.tricks = [] means every dog shares the same array. Put mutable state on instances.
  • Object.create(proto, descriptors) uses property descriptors, not plain values — easy to write { x: 1 } and get a property whose value is { value: undefined } unless you write { x: { value: 1 } }.
  • Arrow functions and methods have no .prototype — only function/class constructors do; new (() => {})() throws.

What a senior is expected to say

  • “Lookup walks [[Prototype]]; writes create own properties. class methods live on the shared .prototype, fields on the instance.”
  • “Never mutate an object’s prototype after creation — it deopts inline caches. Set it via Object.create or class.”
  • Object.create(null) for untrusted maps to avoid prototype pollution; Object.hasOwn to check ownership.”
  • instanceof breaks across realms — use Array.isArray / the toString tag instead.”

Cross-references

Further reading