Prototypal Inheritance
Most languages inherit by copying a blueprint into each instance. JavaScript does something simpler and more powerful: objects delegate to other objects through the prototype chain. Instead of duplicating behavior, an object simply points at another object and borrows its properties at lookup time. Understanding this delegation model lets you reuse code cleanly, and it demystifies what class actually compiles down to.
Inheritance is delegation
When you read a property the engine can’t find on an object, it follows the object’s [[Prototype]] link and looks there instead. So “inheritance” in JavaScript is really live lookup through a chain of objects. Set up that chain and the child object behaves as if it owns the parent’s methods, without ever copying them.
The most direct way to build a chain is Object.create, which makes a new object whose prototype is exactly the object you pass in:
const animal = {
describe() {
return `${this.name} is a ${this.type}`;
},
};
const dog = Object.create(animal);
dog.name = "Rex";
dog.type = "dog";
console.log(dog.describe()); // delegated to animal
console.log(Object.getPrototypeOf(dog) === animal); // true
Output:
Rex is a dog
true
dog owns only name and type. The describe method lives on animal, but this inside it still refers to dog, so delegation reads the right data.
Multi-level chains
Because a prototype is just an object, it can have its own prototype. Chaining Object.create calls builds a hierarchy where each level adds behavior and the next level down delegates upward.
const animal = {
eat() {
return `${this.name} eats`;
},
};
const dog = Object.create(animal);
dog.bark = function () {
return `${this.name} barks`;
};
const puppy = Object.create(dog);
puppy.name = "Milo";
console.log(puppy.bark()); // found on dog
console.log(puppy.eat()); // found on animal
Output:
Milo barks
Milo eats
The chain is puppy → dog → animal → Object.prototype → null. Lookup stops at the first match, so closer prototypes naturally override more distant ones.
Constructor inheritance with Object.create + call
Before class, the canonical way to make one constructor inherit from another used two pieces working together: Parent.call(this, ...) to run the parent’s initializer on the new instance, and Object.create(Parent.prototype) to wire the prototype chain for shared methods.
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function () {
return `${this.name} eats`;
};
function Dog(name, breed) {
Animal.call(this, name); // 1. inherit instance properties
this.breed = breed;
}
// 2. inherit prototype methods
Dog.prototype = Object.create(Animal.prototype);
// 3. restore the constructor reference
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function () {
return `${this.name} barks`;
};
const d = new Dog("Rex", "Lab");
console.log(d.eat());
console.log(d.bark());
console.log(d instanceof Dog, d instanceof Animal);
Output:
Rex eats
Rex barks
true true
Each step matters. Animal.call(this, name) copies own properties onto the new Dog. Object.create(Animal.prototype) makes Dog.prototype delegate to Animal.prototype without invoking Animal. Resetting constructor keeps d.constructor === Dog correct.
A common bug is writing
Dog.prototype = new Animal()instead ofObject.create(Animal.prototype). That runs the parent constructor prematurely and leaks shared state onto the prototype. Always link prototypes withObject.create.
Classes are sugar over this
ES2015 class syntax produces exactly the same prototype wiring shown above — it is not a new object model. extends sets up the prototype chain, super(...) is the Parent.call(this, ...) step, and methods are placed on the prototype.
class Animal {
constructor(name) {
this.name = name;
}
eat() {
return `${this.name} eats`;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // calls Animal's constructor with the new `this`
this.breed = breed;
}
bark() {
return `${this.name} barks`;
}
}
const d = new Dog("Rex", "Lab");
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
console.log(d.eat(), "/", d.bark());
Output:
true
Rex eats / Rex barks
The first log proves the chain is identical to the manual version: Dog.prototype delegates to Animal.prototype. Classes simply make the intent clearer and handle constructor/super bookkeeping for you.
Comparing the approaches
| Technique | Sets up chain | Runs parent init | Best for |
|---|---|---|---|
Object.create(proto) | Yes | No (manual) | Plain object delegation, mixins |
Constructor + Object.create + call | Yes | Yes (via call) | Legacy/constructor-style code |
class ... extends | Yes | Yes (via super) | Modern, readable hierarchies |
Best Practices
- Reach for
class ... extendsin new code; it generates the correct prototype wiring without manualconstructorfixes. - Use
Object.create(proto)for lightweight delegation when you don’t need a constructor. - When writing constructor inheritance by hand, always pair
Parent.call(this, ...)withObject.create(Parent.prototype)and restoreDog.prototype.constructor. - Keep hierarchies shallow; deep chains slow lookups and make
thisharder to reason about. - Prefer composition (mixing in behavior objects) over deep inheritance trees when relationships aren’t truly “is-a”.
- Never assign
Child.prototype = new Parent()— it runs the parent constructor early and shares mutable state across instances.