JavaScript Prototypal Inheritance Explained
Prototypal inheritance means JavaScript objects inherit properties directly from other objects via a prototype chain, not from classes. How it works.
Prototypal inheritance is JavaScript’s mechanism for sharing behavior between objects: instead of copying methods from a class blueprint, an object simply points to another object — its prototype — and looks up any property it doesn’t have locally by walking that link. There’s no compiler-enforced class hierarchy underneath; it’s objects linked to other objects, all the way down to Object.prototype.
The prototype chain
Every JavaScript object has an internal [[Prototype]] link (exposed via Object.getPrototypeOf() or the deprecated __proto__ accessor). When you access a property, the engine first checks the object itself. If it’s not there, it checks the object’s prototype, then that prototype’s prototype, and so on until it either finds the property or reaches null.
const animal = {
speak() {
return `${this.name} makes a sound.`;
},
};
const dog = Object.create(animal);
dog.name = "Rex";
dog.speak(); // "Rex makes a sound."
dog has no speak method of its own. The lookup falls through to animal, finds speak there, and calls it with dog as this. That’s the entire mechanism — no hidden magic, just a linked list of objects.
Constructor functions and new
Before ES6 classes, prototypal inheritance was usually set up through constructor functions and their .prototype property:
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function () {
return `${this.name} makes a sound.`;
};
function Dog(name) {
Animal.call(this, name);
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
const rex = new Dog("Rex");
rex.speak(); // "Rex makes a sound."
Calling a function with new creates a new object, sets its [[Prototype]] to the constructor’s .prototype object, and runs the constructor with this bound to the new object. Every function you write has a .prototype property sitting there, ready to be used this way, whether or not you ever call it with new.
Classes are prototypes with a nicer syntax
ES6 class syntax didn’t add a new inheritance model — it’s sugar over the same prototype chain:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound.`;
}
}
class Dog extends Animal {}
const rex = new Dog("Rex");
rex.speak(); // "Rex makes a sound."
Under the hood, class Dog extends Animal sets Dog.prototype’s [[Prototype]] to Animal.prototype, exactly like the Object.create line in the constructor-function version. Methods defined in a class body land on the prototype, not on individual instances, so every Dog instance shares one copy of speak rather than carrying its own. This matters for anyone reading modern TypeScript or JavaScript codebases: the class keyword reads like classical inheritance from languages like Java, but the runtime behavior underneath is still prototype delegation, with all its quirks intact.
Prototypal vs classical inheritance
| Classical (Java, C++) | Prototypal (JavaScript) | |
|---|---|---|
| What you inherit from | A class (a template) | Another object (a live instance) |
| Relationship | Instance-of a class | Delegates-to a prototype |
| Changeable at runtime | No — class is fixed at compile time | Yes — reassign [[Prototype]] anytime |
| Sharing behavior | Copy method definitions into subclass | Look up method through the chain |
| Multiple inheritance | Usually restricted or forbidden | Achievable via mixins/composition |
The practical difference shows up when you need flexibility: prototypes can be reshaped at runtime, and objects can delegate to more than one source through composition patterns (mixins), something classical single-inheritance hierarchies make awkward.
Where this trips people up
thisbinding. Prototype methods run with whateverthisthe call site provides, not the object where the method was defined. Detaching a method (const fn = dog.speak; fn()) loses that binding — a common source of bugs, especially with event handlers. Closures and arrow functions are the usual fixes, since arrow functions capturethislexically instead of rebinding it per call.- Shared mutable state. Properties defined on a prototype (rather than assigned in the constructor) are shared by every instance. Mutating an array or object stored directly on a prototype affects all instances that delegate to it — a frequent source of subtle bugs.
- Performance. Long prototype chains mean more lookups for properties that aren’t found immediately. In practice this rarely matters outside tight loops, but it’s why flat object shapes are still recommended in hot paths.
Object.create(null). You can create an object with no prototype at all — notoString, nohasOwnProperty, nothing inherited. Useful for plain dictionaries where you don’t want prototype pollution to be a concern, a technique that shows up in security-conscious code alongside topics like XSS prevention.
Proxies and dynamic behavior
Because the prototype chain is just object references, JavaScript lets you intercept the entire lookup process with a Proxy, trapping property access before it even reaches the prototype. That’s a more advanced technique, but it underscores the same theme: JavaScript’s object model is deliberately dynamic, in contrast to the fixed class layouts of most classical OOP languages.
The takeaway
Prototypal inheritance means every JavaScript object delegates to another object through a [[Prototype]] link, and property lookups walk that chain until they find a match or hit null. class syntax doesn’t change this model — it’s a readable wrapper around the same constructor-function-and-prototype mechanics JavaScript has always had. Understanding the chain explains why methods are shared instead of copied, why this depends on the call site rather than where a function was defined, and why JavaScript’s object system stays flexible enough to reshape at runtime in ways classical inheritance never allows.
Tagged
Keep reading
Takina · · 5 min read Promise.all() vs allSettled() vs race() Compared
Promise.all() fails fast, allSettled() waits for every result, and race() returns whichever promise finishes first — how to choose correctly.
Takina · · 4 min read JavaScript Spread vs Rest Operators, Explained
The spread operator (...) expands an iterable into individual elements; the rest operator collects elements back into an array. Same syntax, opposite jobs.
Takina · · 4 min read ResizeObserver API Explained: Watching Element Size
The ResizeObserver API lets JavaScript watch an element's box size and react without polling or resize-event hacks. How it works and when to use it.