Prototypes and the Prototype Chain

Updated 4 min read source
On this page6
  1. TL;DR
  2. In depth
  3. Gotchas / edge cases
  4. What a senior is expected to say
  5. Cross-references
  6. Further reading

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.

In depth

What is the prototype chain?

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.

js
const animal = { eats: true };
// dog.[[Prototype]] === animal
const dog = Object.create(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).

__proto__ vs prototype vs [[Prototype]]?

  • [[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.
js
function Dog() {}
const d = new Dog();
Object.getPrototypeOf(d) === Dog.prototype;  // true

How does class map onto prototypes?

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

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

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

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

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.

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

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

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

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

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.

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

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 Frontend Security — Senior Interview Prep.
  • 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 4

  • “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