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
thisbound 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 restoreconstructor. - Override by shadowing; extend by calling the parent method with
.call(this)(the pre-classsuper). - Delegation shares one method copy live; concatenation/mixins copy methods in flat.
- Never set
Child.prototype = Parent.prototype— useObject.create(Parent.prototype).
Everything class does, this model does underneath. Learn it directly and extends,
super, and the rest become transparent rather than magical.