JavaScript · Objects & Prototypes

Prototypal Inheritance in JavaScript — The Complete Practical Guide

7 min read Updated 2026-06-18

Practice Prototypal Inheritance interview questions

Inheritance without classes

Most languages implement inheritance through classes: a blueprint that other blueprints extend. JavaScript takes a different, arguably simpler route — objects inherit directly from other objects through the prototype chain. This is prototypal inheritance, and it's what class quietly compiles down to. Understanding it directly (not just through the class veneer) gives you a much firmer grip on how behavior is shared, overridden, and extended in JavaScript.

The core idea is delegation: an object delegates property and method lookups it can't satisfy to its prototype. There's no copying of methods into each object — instances point back to shared behavior and borrow it on demand.

Delegation, the foundation

When an object lacks a property, lookup climbs to its prototype. So if you put shared methods on a prototype object, every object linked to it can use them as if they were its own.

const animal = {
  describe() { return `${this.name} is a ${this.type}` }
}

const dog = Object.create(animal)
dog.name = 'Rex'
dog.type = 'dog'

dog.describe()   // 'Rex is a dog' method delegated to animal

Notice this inside describe refers to dog, the object the method was called on, not animal where it lives. That's the magic of delegation: one shared method, many objects, each supplying its own data through this.

Building inheritance with Object.create

The cleanest way to express "B inherits from A" with plain objects is to set B's prototype to A. You can build multi-level hierarchies this way.

const shape = {
  area() { return 0 },
  toString() { return `${this.constructor?.name ?? 'Shape'} area=${this.area()}` }
}

const rectangle = Object.create(shape)
rectangle.init = function (w, h) { this.w = w; this.h = h; return this }
rectangle.area = function () { return this.w * this.h }   // override

const r = Object.create(rectangle).init(3, 4)
r.area()   // 12 — rectangle's own area

Here the chain is r -> rectangle -> shape -> Object.prototype. r borrows init and area from rectangle, which itself delegates anything missing to shape.

Constructor-function inheritance

Before class, the standard pattern used constructor functions and explicitly wired up the prototype chain. It's verbose but worth understanding because class does exactly this under the hood.

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

function Dog(name) {
  Animal.call(this, name)         // 1. run parent constructor on `this`
}
Dog.prototype = Object.create(Animal.prototype)  // 2. link prototypes
Dog.prototype.constructor = Dog                    // 3. fix the constructor pointer
Dog.prototype.speak = function () { return `${this.name} barks` }  // override

const d = new Dog('Rex')
d.speak()              // 'Rex barks'
d instanceof Animal    // true chain set up correctly

The three steps matter: (1) call the parent constructor with Animal.call(this, ...) so instance fields are initialized, (2) set Dog.prototype to an object that delegates to Animal.prototype, and (3) restore constructor. Skip any of them and you get subtle bugs.

Overriding and extending methods

Overriding is just shadowing: define a method with the same name lower in the chain and it wins. To extend rather than replace the parent's behavior, call up to the parent explicitly.

Dog.prototype.speak = function () {
  const base = Animal.prototype.speak.call(this)  // reuse parent logic
  return `${base}, specifically a bark`
}

Animal.prototype.speak.call(this) is the pre-class equivalent of super.speak(). You fetch the parent method off the parent prototype and invoke it with the current this. With class, super does this for you — but it's the same mechanism.

Delegation vs concatenation (mixins)

There are two broad strategies for sharing behavior:

  • Delegation (prototypal): objects link to a shared prototype and borrow methods live. One copy of each method, changes to the prototype are seen instantly by all instances.
  • Concatenation (mixins): copy properties from source objects into the target with Object.assign. Each object gets its own copy; no live link.
// concatenation / mixin
const serializable = { toJSON() { return JSON.stringify(this) } }
const comparable   = { equals(o) { return this.id === o.id } }

const entity = Object.assign({}, serializable, comparable, { id: 1 })

Delegation is memory-efficient and dynamic; concatenation is flat and avoids deep chains. Real codebases use both — prototypes for primary inheritance, mixins for cross-cutting capabilities that don't fit a single hierarchy.

The instanceof relationship

Because inheritance is just chained prototypes, instanceof reflects the whole hierarchy.

const d = new Dog('Rex')
d instanceof Dog      // true
d instanceof Animal   // true Animal.prototype is in the chain
d instanceof Object   // true

instanceof checks whether the right-hand side's .prototype appears anywhere in the left-hand side's chain — so it naturally returns true for every ancestor.

Common pitfalls

A few mistakes recur constantly:

// forgetting Object.create — this points the SAME object, not a copy
Dog.prototype = Animal.prototype
// now overriding Dog.prototype.speak also changes Animal's! shared reference

// forgetting to call the parent constructor
function Dog(name) { /* missing Animal.call(this, name) */ }
new Dog('Rex').name   // undefined — instance never initialized

The first is the most dangerous: assigning Dog.prototype = Animal.prototype makes them the same object, so changes leak between parent and child. Always use Object.create(Animal.prototype) to get a fresh delegating object. The second leaves instance fields uninitialized because the parent's setup code never ran.

Another subtle one: defining methods on instances instead of the prototype wastes memory and breaks the "shared behavior" model.

function Cat(name) {
  this.name = name
  this.meow = function () { return 'meow' }  // new function per instance
}

Move meow to Cat.prototype so all cats share it.

When prototypal shines

Prototypal inheritance excels when you want flexible, runtime-adjustable behavior: objects can be composed and re-linked dynamically, behavior can be added to a prototype and instantly seen by all instances, and there's no rigid class hierarchy to fight. It's also just less machinery — objects all the way down. The tradeoff is that the explicit constructor-wiring pattern is verbose, which is exactly why class syntax was added as sugar.

Key takeaways

  • JavaScript inheritance is delegation: objects borrow methods from their prototype, with this bound to the calling object.
  • Object.create(proto) is the cleanest primitive for "inherit from this object."
  • Constructor inheritance needs three steps: call the parent constructor, link prototypes via Object.create, and restore constructor.
  • Override by shadowing; extend by calling the parent method with .call(this) (the pre-class super).
  • Delegation shares one method copy live; concatenation/mixins copy methods in flat.
  • Never set Child.prototype = Parent.prototype — use Object.create(Parent.prototype).

Everything class does, this model does underneath. Learn it directly and extends, super, and the rest become transparent rather than magical.

Practice tests are coming soon

Get notified when interactive mock interviews and quizzes launch.