JavaScript Prototypes and Prototype Inheritance
Understand the JavaScript prototype chain, how Object.create works, what class syntax desugars to, and why this still matters in modern code.
What you'll learn
- ✓What the prototype chain actually is
- ✓How Object.create builds prototype links directly
- ✓How class syntax desugars to constructor functions and prototypes
- ✓What instanceof checks and where it lies
- ✓Why understanding prototypes still pays off in modern JavaScript
Prerequisites
- •Comfort with JavaScript objects
- •Familiarity with functions and constructors
JavaScript does not really have classes. It has objects that link to other objects, and a class keyword that papers over the linking with familiar syntax. If you want to read framework source code, debug a “why is this method missing” error, or build performant abstractions, you need to understand the prototype chain. The good news is that it is a small idea with big consequences.
Objects link to objects
Every object in JavaScript has a hidden link to another object called its prototype. When you read a property and the engine does not find it on the object itself, it follows that link and looks on the prototype. If the prototype does not have it either, the engine follows the next link, and so on until it hits null.
That chain is the prototype chain. Method calls, property lookups, even toString() all walk this chain.
const animal = { eats: true };
const dog = Object.create(animal);
dog.barks = true;
console.log(dog.barks); // true (own property)
console.log(dog.eats); // true (found on animal via the chain)
console.log(dog.flies); // undefined (not found anywhere)
Object.create(animal) creates a new object whose prototype link points at animal. Nothing more, nothing less. You can inspect the link with Object.getPrototypeOf(dog) which returns animal.
Why prototypes exist
Prototypes are how JavaScript shares behavior between objects without copying it. If every dog had its own copy of every method, a thousand dogs would mean a thousand copies of bark. With a prototype, every dog points at the same shared object and gets the method for free.
const dogMethods = {
bark() { console.log(`${this.name} says woof`); },
};
function makeDog(name) {
const dog = Object.create(dogMethods);
dog.name = name;
return dog;
}
const a = makeDog("Rex");
const b = makeDog("Luna");
a.bark(); // Rex says woof
b.bark(); // Luna says woof
Both dogs share one bark. They each have their own name. That separation between own state and shared behavior is the whole point.
Constructor functions and prototype
Before classes, the standard pattern used a constructor function plus its prototype property.
function Dog(name) {
this.name = name;
}
Dog.prototype.bark = function () {
console.log(`${this.name} says woof`);
};
const rex = new Dog("Rex");
rex.bark(); // Rex says woof
When you call new Dog(...), JavaScript does four things: it creates a new object, links the new object’s prototype to Dog.prototype, sets this to the new object, and runs the function body. If the function returns nothing, new returns the new object.
So rex has no own bark. The lookup finds it on Dog.prototype. This is identical to the Object.create example, just with sugar.
Classes are sugar
The class keyword arrived in ES2015. It is genuinely useful, but it does not introduce a new inheritance model. It is sugar over the same constructor-plus-prototype mechanism.
class Dog {
constructor(name) {
this.name = name;
}
bark() {
console.log(`${this.name} says woof`);
}
}
That code is, almost line for line, the constructor function above. bark lives on Dog.prototype, not on each instance. You can prove it:
const rex = new Dog("Rex");
console.log(Object.hasOwn(rex, "bark")); // false
console.log(Dog.prototype.bark === rex.bark); // true
Subclassing with extends builds a chain of prototypes:
class Animal {
eat() { console.log("munch"); }
}
class Dog extends Animal {
bark() { console.log("woof"); }
}
const rex = new Dog();
rex.bark(); // own class
rex.eat(); // walks up to Animal.prototype
Dog.prototype’s prototype is Animal.prototype. When you call rex.eat(), the engine walks Dog, then Animal, finds eat, and runs it with this set to rex.
instanceof and its lies
instanceof does not check types. It walks the prototype chain of the left side and asks whether the right side’s prototype is anywhere on it.
class Dog {}
const rex = new Dog();
console.log(rex instanceof Dog); // true
console.log(rex instanceof Object); // true
This is usually what you want. But instanceof lies in two situations.
First, across realms. If you pass an object between an iframe and its parent, each realm has its own Array, Object, and so on. value instanceof Array will return false even though it walks and quacks like an array. Use Array.isArray(value) instead.
Second, you can manually break the chain with Object.setPrototypeOf, and instanceof will follow the new chain. Avoid mutating prototypes after creation. It defeats engine optimizations and confuses every reader.
Reading the chain in the console
In the browser console, expand any object and you will see a [[Prototype]] entry. Click through it and you can walk the chain by hand. This is the fastest way to understand any unfamiliar library.
const arr = [1, 2, 3];
Object.getPrototypeOf(arr) === Array.prototype; // true
Object.getPrototypeOf(Array.prototype) === Object.prototype; // true
Object.getPrototypeOf(Object.prototype) === null; // true
Three links and you are at the top. Every array in the language uses this exact chain. The methods you call on arrays like map and filter live on Array.prototype.
A practical pattern: mixins
Prototypes also let you compose behavior without single inheritance.
const Serializable = {
toJSON() { return JSON.stringify(this); },
};
const Loggable = {
log() { console.log(this.toJSON()); },
};
class User {
constructor(name) { this.name = name; }
}
Object.assign(User.prototype, Serializable, Loggable);
new User("Ada").log(); // {"name":"Ada"}
You are copying methods onto the prototype directly. This is not as clean as class extension, but it is the only practical way to pull in behavior from multiple sources. Libraries do this all the time.
Why this still matters
In a world of class syntax and TypeScript, why bother with prototypes? Three reasons.
You will read framework source. React, Vue, and most ORMs lean on prototype tricks for performance and ergonomics. If you cannot read Object.create and Object.getPrototypeOf, you will struggle.
You will hit weird bugs. “Why does my array have a method I never defined?” Because something extended Array.prototype. “Why does JSON.stringify skip a field?” Because the field is on the prototype, not the instance. These are five-second debugging sessions once you know what to look for.
You will write better code. Knowing that methods live on the prototype tells you why class fields with arrow functions are different from class methods, and why the difference matters for memory. The first creates a function per instance, the second shares one function across all instances. Combine this with what you know about arrow functions and this and the tradeoffs become obvious.
Wrap up
Prototypes are JavaScript’s actual inheritance model. Objects link to other objects, and the engine walks those links when you read a property. Constructor functions, class, extends, and instanceof are all built on top of this one mechanism. Spend an hour playing with Object.create, inspect a class in your console, and watch what Object.getPrototypeOf returns at every step. After that, the language stops surprising you, and the next nasty bug becomes a five-minute fix instead of a two-hour rabbit hole.