Back to JavaScript and TypeScript

Prototype Chain and Inheritance

JavaScript has no classical inheritance. Objects simply link to other objects through a hidden [[Prototype]] slot. This guide shows you exactly how that works and why class is just a friendlier way to write it.

Fundamentals

Chain Mechanics

Every object has a hidden [[Prototype]] slot pointing to another object (or null). Property lookups walk this chain until the property is found or the end is reached.

Every object has a prototype

When you access a property, JavaScript first looks on the object itself. If it's not there, it walks up the [[Prototype]] chain. Object.getPrototypeOf(obj) returns that parent.

const animal = {
  breathe() { return "inhale → exhale" }
}

const dog = Object.create(animal)
dog.bark = function() { return "woof" }

console.log(dog.bark())    // "woof"    own property
console.log(dog.breathe()) // "inhale → exhale"   found on prototype
console.log(Object.getPrototypeOf(dog) === animal) // true

The prototype chain ends at null

Object.prototype sits at the top of nearly every chain. Its prototype is null the end of the line. That's where toString, hasOwnProperty, and friends live.

const obj = { x: 1 }

// Chain: obj → Object.prototype → null
console.log(Object.getPrototypeOf(obj) === Object.prototype) // true
console.log(Object.getPrototypeOf(Object.prototype))         // null

// hasOwnProperty lives on Object.prototype
console.log(obj.hasOwnProperty("x")) // true   inherited method
console.log(obj.hasOwnProperty("y")) // false

Property shadowing

If an object defines a property with the same name as one on its prototype, the object's own version wins. The prototype version is shadowed not overwritten.

const base = { greet() { return "Hello from base" } }
const child = Object.create(base)

// Before shadowing
console.log(child.greet()) // "Hello from base"

// Add own property  shadows the prototype version
child.greet = function() { return "Hello from child" }
console.log(child.greet())      // "Hello from child"
console.log(base.greet())       // "Hello from base"   unchanged

Syntax

Constructors and class

Constructor functions and class syntax are two ways to set up prototype chains. class is purely syntax sugar the runtime result is identical.

Constructor functions and .prototype

Every function has a .prototype property (an object). When you call a function with new, the created instance's [[Prototype]] is set to that .prototype object.

function Person(name) {
  this.name = name
}

Person.prototype.greet = function() {
  return `Hi, I'm ${this.name}`
}

const alice = new Person("Alice")
console.log(alice.greet())  // "Hi, I'm Alice"

// alice's [[Prototype]] is Person.prototype
console.log(Object.getPrototypeOf(alice) === Person.prototype) // true

class is syntactic sugar

class syntax compiles down to the same prototype-based mechanics. The methods go on ClassName.prototype, not on each instance. Nothing new happens under the hood.

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

class Dog extends Animal {
  speak() { return `${this.name} barks` }
}

const d = new Dog("Rex")
console.log(d.speak())  // "Rex barks"

// Chain: d → Dog.prototype → Animal.prototype → Object.prototype → null
console.log(d instanceof Dog)    // true
console.log(d instanceof Animal) // true

What new actually does

  1. Creates a new empty object.
  2. Sets its [[Prototype]] to Constructor.prototype.
  3. Calls the constructor with this bound to that new object.
  4. Returns the new object (unless the constructor returns a different object explicitly).

Watch Out

Common Gotchas

Prototype mechanics are simple, but a few behaviour patterns reliably catch developers off guard.

Mutating a shared prototype affects all instances

Because instances share the same prototype object, adding or changing a method on Foo.prototype after construction will instantly affect every existing instance.

function Foo() {}
const a = new Foo()
const b = new Foo()

// Add a method after instances are created
Foo.prototype.sayHi = function() { return "hi" }

console.log(a.sayHi()) // "hi"   picks it up immediately
console.log(b.sayHi()) // "hi"

for...in walks the whole chain

for...in includes inherited enumerable properties. Use hasOwnProperty (or Object.keys) if you only want the object's own properties.

const parent = { inherited: true }
const child = Object.create(parent)
child.own = true

for (const key in child) {
  console.log(key) // "own", then "inherited"
}

// Only own properties:
Object.keys(child).forEach(k => console.log(k)) // "own"

instanceof checks the chain, not identity

instanceof walks the prototype chain checking if Constructor.prototype appears anywhere. It does not check the constructor function itself, which can surprise you across iframes or realms.

class A {}
class B extends A {}
class C {}

const b = new B()
console.log(b instanceof B) // true
console.log(b instanceof A) // true   chain includes A.prototype
console.log(b instanceof C) // false

// You can customise instanceof with Symbol.hasInstance
class Even {
  static [Symbol.hasInstance](n) { return n % 2 === 0 }
}
console.log(2 instanceof Even) // true
console.log(3 instanceof Even) // false