intermediate

Prototypal Inheritance and the Prototype Chain in JavaScript

How the prototype chain really works — __proto__ vs prototype, property lookup, what new and class do under the hood, and Object.create interview questions.


The one-sentence model

Every object has an internal link ([[Prototype]]) to another object. When you read a property that the object doesn't own, JavaScript walks up that chain of links until it finds the property or reaches null. That's the entire inheritance system — everything else (class, new, extends) is syntax and conventions layered on top.

const animal = {
  eats: true,
  describe() {
    return `eats: ${this.eats}`;
  },
};
 
const rabbit = Object.create(animal); // rabbit --[[Prototype]]--> animal
rabbit.hops = true;
 
rabbit.eats;       // true — found one level up the chain
rabbit.describe(); // "eats: true" — this is rabbit (call-site rule)
rabbit.hasOwnProperty("eats"); // false

Note the this behavior: even though describe lives on animal, this is rabbit because of the call site — the implicit binding rule.

prototype vs proto: the question everyone gets wrong

These are two different things with confusingly similar names:

  • __proto__ (spec: [[Prototype]], accessed properly with Object.getPrototypeOf) — the actual link an object uses for lookup. Every object has one.
  • prototype — a plain property that only functions have. It's not the function's own prototype link; it's the object that will be assigned as [[Prototype]] of instances created by calling that function with new.
function User(name) {
  this.name = name;
}
 
const u = new User("Ada");
 
Object.getPrototypeOf(u) === User.prototype; // true
Object.getPrototypeOf(User) === Function.prototype; // true — different chain!

What new actually does

A four-step answer that interviewers accept as complete:

function myNew(Constructor, ...args) {
  // 1. Create a fresh object linked to Constructor.prototype
  const obj = Object.create(Constructor.prototype);
  // 2. Call the constructor with this = obj
  const result = Constructor.apply(obj, args);
  // 3. If the constructor returned an object, use it...
  // 4. ...otherwise return the created object
  return result !== null && typeof result === "object" ? result : obj;
}

Implementing myNew is itself a common interview exercise, and it composes with custom apply and the new-edge-case in custom bind.

Property lookup, shadowing, and writes

Reads walk the chain; writes don't. Assigning creates an own property that shadows the inherited one:

const defaults = { theme: "dark" };
const settings = Object.create(defaults);
 
settings.theme;          // "dark"   — inherited
settings.theme = "light";
settings.theme;          // "light"  — own property now shadows
defaults.theme;          // "dark"   — untouched

Follow-up trap: methods that mutate through an inherited reference do affect the shared object:

const base = { list: [] };
const child = Object.create(base);
child.list.push(1);      // mutates base.list — no shadowing on mutation
base.list;               // [1]

This is exactly why shared mutable state on prototypes is an anti-pattern.

class is prototypes with better ergonomics

class Animal {
  constructor(name) { this.name = name; }
  speak() { return `${this.name} makes a sound`; }
}
 
class Dog extends Animal {
  speak() { return `${this.name} barks`; }
}

Under the hood:

  • Dog.prototype is an object whose [[Prototype]] is Animal.prototype (method inheritance).
  • Dog itself links to Animal (static inheritance).
  • super.speak() looks up the chain starting above Dog.prototype.
  • Methods are non-enumerable, and class bodies run in strict mode — two real differences from the pre-ES6 function pattern, along with the requirement to call new (classes throw without it).

An interviewer who asks "what does class do that constructor functions don't?" wants those last three differences, not "it's just sugar."

Object.create(null): the dictionary trick

Object.create(null) makes an object with no prototype at all — no inherited toString, no hasOwnProperty, and crucially no risk of prototype-key collisions:

const dict = Object.create(null);
dict["__proto__"] = "safe here"; // just a normal key
"toString" in dict;              // false

Mentioning this (and prototype-pollution attacks it defends against) is a strong senior signal. It also matters when implementing custom deep clone — do you preserve the prototype or not?

Interview follow-ups

  • "How do you check if a property is own vs inherited?"Object.hasOwn(obj, key) (ES2022) or hasOwnProperty; in checks the whole chain.
  • "What's the end of the chain?"Object.prototype, whose prototype is null.
  • "How would you implement instanceof?" — walk Object.getPrototypeOf in a loop comparing against Constructor.prototype.
  • "Performance?" — long chains slow lookups and deoptimize inline caches; keep hierarchies shallow, prefer composition.

  • The Event Loop

    intermediate

    How the call stack, microtask queue, and macrotask queue decide execution order — with the output-prediction questions interviewers love.

  • Closures

    beginner

    What a closure really is, why loops with var trip people up, and how closures power memoization, once(), and module patterns.

  • The this Keyword

    intermediate

    The four binding rules, arrow-function behavior, and the lost-this bugs that call, apply, and bind exist to fix.