ESC

Type to search the knowledge base.

Prototypal Inheritance

How JS objects delegate via [[Prototype]] — chains, Object.create, constructors, classes as sugar, own vs inherited props, and common footguns.

intermediate6 min read
  • javascript
  • prototype
  • inheritance
  • objects

JavaScript’s core object model is delegation, not classical copy-the-template inheritance. Every ordinary object has an internal link [[Prototype]] (exposed as __proto__ or via Object.getPrototypeOf). When you read a property the object does not own, the engine walks the chain until it finds the property or hits null.

That link is prototypal inheritance. Classes, constructors, and extends are syntax and conventions on top of the same chain.

If you only memorize “__proto__ vs prototype,” you’ll still fail the interview follow-up: where does a method lookup go, and what does assignment write?

The problem this model solves

You want many objects to share behavior without cloning function bodies onto each instance:

const animal = {
  eat() {
    return `${this.name} eats`;
  },
};

const dog = Object.create(animal);
dog.name = 'Rex';
dog.eat(); // "Rex eats" — method found on animal, this === dog

One eat function, many animals. Memory stays flat; behavior stays consistent.

Model: own properties first, then the chain

const proto = { kind: 'proto', shared: true };
const obj = Object.create(proto);
obj.kind = 'own';

console.log(obj.kind);   // "own" — own property wins
console.log(obj.shared); // true — inherited
console.log(obj.missing); // undefined — chain ended
Operation Behavior
Get obj.x Own → [[Prototype]] → … → null
Set obj.x = v Usually creates/updates own property (does not edit the prototype’s x unless it’s an accessor with a setter)
in operator True if found anywhere on the chain
hasOwnProperty / Object.hasOwn True only for own properties
const p = { a: 1 };
const c = Object.create(p);
console.log('a' in c); // true
console.log(Object.hasOwn(c, 'a')); // false
c.a = 2; // shadows — own `a` now
console.log(p.a); // 1 — prototype unchanged

Shadowing is how instances customize shared defaults without mutating the prototype for everyone.

__proto__ vs .prototype (say this cleanly)

Two different things people overload with the word “prototype”:

  1. obj.[[Prototype]] — the object used for lookup on obj. Read with Object.getPrototypeOf(obj).
  2. Fn.prototype — the object that will become [[Prototype]] of instances created with new Fn().
function Person(name) {
  this.name = name;
}
Person.prototype.greet = function () {
  return `Hi, ${this.name}`;
};

const p = new Person('Ada');
Object.getPrototypeOf(p) === Person.prototype; // true
p.greet(); // "Hi, Ada"
p.hasOwnProperty('greet'); // false — method lives on Person.prototype

new roughly:

  1. Create a new object.
  2. Set its [[Prototype]] to Constructor.prototype.
  3. Call Constructor with this bound to that object.
  4. Return the object (unless the constructor returns another object).

More on construction and this: this Binding Rules.

Object.create and null objects

const dict = Object.create(null);
dict.answer = 42;
// dict.toString — undefined; no Object.prototype
console.log('toString' in dict); // false

Null-prototype objects are excellent as pure maps (no inherited keys, no constructor surprises). Prefer Map when key types or iteration ergonomics matter; use Object.create(null) when you need a plain JSON-like bag without prototype pollution footguns.

// Dangerous if keys come from users and you inherit Object.prototype
const bag = {};
// bag['__proto__'] = ... historical issues; also hasOwn checks matter

Constructor chains before class

function Animal(name) {
  this.name = name;
}
Animal.prototype.speak = function () {
  return `${this.name} makes a noise`;
};

function Dog(name, breed) {
  Animal.call(this, name); // own props from parent
  this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.speak = function () {
  return `${this.name} barks`;
};

const d = new Dog('Rex', 'mutt');
d.speak(); // "Rex barks"
d instanceof Dog; // true
d instanceof Animal; // true

Pattern pieces:

  • Parent.call(this, …) initializes own state.
  • Child.prototype = Object.create(Parent.prototype) wires delegation.
  • Reset constructor if you care about instance.constructor.
  • Put methods on .prototype, not on this inside the constructor (unless per-instance state requires it).

call here is explicit binding — call, apply, and bind.

class is mostly sugar (know the desugaring)

class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    return `${this.name} makes a noise`;
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }
  speak() {
    return `${this.name} barks`;
  }
}

Under the hood you still get prototype links. Differences that matter:

  • Class bodies run in strict mode.
  • Methods are non-enumerable.
  • super has precise lookup rules.
  • Classes are not hoisted like function declarations — they sit in the TDZ.
  • Calling a class without new throws.
typeof Dog; // "function"
Object.getPrototypeOf(Dog.prototype) === Animal.prototype; // true
Object.getPrototypeOf(Dog) === Animal; // static side chain

Static methods live on the constructor function, not on instances.

Property lookup vs assignment (the classic trap)

function Counter() {}
Counter.prototype.count = 0;

const a = new Counter();
const b = new Counter();
a.count += 1; // read 0 from proto, write own a.count = 1
console.log(a.count); // 1
console.log(b.count); // 0 — still on prototype
console.log(Counter.prototype.count); // 0

Mutating object values on a shared prototype is worse:

function Team() {}
Team.prototype.members = []; // shared mutable — usually a bug

const t1 = new Team();
const t2 = new Team();
t1.members.push('A');
console.log(t2.members); // ['A'] — surprise coupling

Initialize mutable state in the constructor (this.members = []), share only immutable defaults or methods on the prototype.

instanceof and isPrototypeOf

d instanceof Dog; // walks d's chain looking for Dog.prototype
Dog.prototype.isPrototypeOf(d); // true

instanceof can be faked with Symbol.hasInstance. For pure chain checks, isPrototypeOf is direct. Across realms (iframes), instanceof Array can fail — prefer Array.isArray.

Modern alternatives you should name

Need Prefer
Share behavior Prototype methods / class
Compose behavior Mixins carefully, or plain functions + closures
Dictionary Map or Object.create(null)
Privacy Closures or #private fields
Multiple “parents” Composition over deep prototype trees

Prototypes are not bad. Deep fragile hierarchies are. Frontend codebases usually win with shallow prototypes (one class or factory) and composition for cross-cutting behavior.

Footguns

  1. Confusing Fn.prototype with instance.__proto__ — related, not identical names.
  2. Mutable state on .prototype.
  3. Forgetting Parent.call(this) when hand-rolling inheritance — child instances miss parent own fields.
  4. Arrow methods on prototypes via class fields — class C { m = () => this } puts a per-instance arrow (often intentional for this, costly if overused).
  5. Iterating with for…in without hasOwn filters — picks up enumerable inherited keys.
  6. Assuming JSON or structured clones keep prototype methods — they don’t; you get data shapes, not class instances.

Interview angle

Prompt: “Explain prototypal inheritance.”

Strong answer: “Objects have an internal [[Prototype]] link. Property reads walk that chain; writes usually create own properties. Constructors and classes set up instance → Constructor.prototype → … → Object.prototype → null. Methods live on the shared prototype so instances share one function. Object.create(proto) builds delegation without a constructor. class/extends desugar to the same links with stricter syntax.”

Whiteboard: Draw the chain for class Dog extends Animal, then predict hasOwn for a method name vs an own field.

Follow-ups: difference between __proto__ and prototype; why instanceof fails across frames; how closures give privacy without prototypes.

Further reading

Related guides