JavaScript Prototypes and the Prototype Chain Explained (with `class` Decoded)
JavaScript is a prototype-based language wearing a class-based costume. The class keyword added in ES2015 is syntactic sugar over a much older mechanism: prototypal inheritance. If you only know class, parts of the language will surprise you. If you know prototypes, class becomes obvious and tools like Object.create, Object.getPrototypeOf, and Reflect stop feeling esoteric.
This is Part 12 of the JavaScript Fundamentals series. We unpack what a prototype actually is, how method lookup works, and how class desugars.
Every object has a prototype #
Every JavaScript object has an internal slot called [[Prototype]] that points to another object (or null). When you look up a property and the object does not have it, the engine walks up the prototype to look there. That's the prototype chain.
const animal = { eats: true };
const rabbit = { jumps: true };
Object.setPrototypeOf(rabbit, animal);
rabbit.jumps; // true — own property
rabbit.eats; // true — inherited from animal
The lookup walks:
- Does
rabbithaveeats? No. - What's
rabbit's[[Prototype]]? It'sanimal. - Does
animalhaveeats? Yes. Return it.
If the chain ended with null and the property wasn't found, you get undefined.
Three ways to set the prototype #
// 1. Object literal — prototype is Object.prototype by default
const a = { x: 1 };
Object.getPrototypeOf(a); // Object.prototype
// 2. Object.create — pick the prototype explicitly
const b = Object.create(a);
Object.getPrototypeOf(b); // a
// 3. Constructor + new — prototype is constructor.prototype
function Cat() {}
const kitty = new Cat();
Object.getPrototypeOf(kitty); // Cat.prototype
Do NOT use __proto__ (the legacy access pattern). Use Object.getPrototypeOf / Object.setPrototypeOf or the constructor form.
Mutating an existing object's prototype with setPrototypeOf is technically allowed but very slow — V8 deoptimizes everything that touches that object. Pick the prototype at creation time and leave it.
The function/prototype duality #
Functions in JavaScript have a .prototype property — an object that becomes the [[Prototype]] of every instance created with new:
function Dog(name) { this.name = name; }
Dog.prototype.bark = function () { return `${this.name} says woof`; };
const rex = new Dog('Rex');
rex.bark(); // 'Rex says woof'
Object.getPrototypeOf(rex) === Dog.prototype; // true
What happens during new Dog('Rex') (from Part 5):
- New empty object created.
- New object's
[[Prototype]]set toDog.prototype. Dogruns withthisbound to the new object.- If
Dogdoes not return an object, the new object is returned.
The bark method lives ONCE on Dog.prototype. Every instance shares it. This is why prototype-based inheritance is memory-efficient compared to copying methods onto each instance.
The prototype chain in action #
function Animal() {}
Animal.prototype.eat = function () { return 'eating'; };
function Dog(name) { this.name = name; }
Object.setPrototypeOf(Dog.prototype, Animal.prototype); // Dog inherits from Animal
Dog.prototype.bark = function () { return 'woof'; };
const rex = new Dog('Rex');
rex.bark(); // 'woof' — found on Dog.prototype
rex.eat(); // 'eating' — found on Animal.prototype (up the chain)
rex.toString(); // '[object Object]' — found on Object.prototype (top of chain)
The chain for rex:
rex ──▶ Dog.prototype ──▶ Animal.prototype ──▶ Object.prototype ──▶ null
Every non-null prototype chain eventually ends at Object.prototype, which holds .toString, .hasOwnProperty, etc.
class decoded #
What ES2015 class syntax actually compiles to:
class Animal {
constructor(name) { this.name = name; }
eat() { return `${this.name} is eating`; }
}
class Dog extends Animal {
bark() { return `${this.name} woofs`; }
}
const rex = new Dog('Rex');
rex.bark(); // 'Rex woofs'
rex.eat(); // 'Rex is eating'
Is almost exactly equivalent to:
function Animal(name) { this.name = name; }
Animal.prototype.eat = function () { return `${this.name} is eating`; };
function Dog(name) { Animal.call(this, name); }
Object.setPrototypeOf(Dog.prototype, Animal.prototype);
Object.setPrototypeOf(Dog, Animal); // ← static inheritance
Dog.prototype.bark = function () { return `${this.name} woofs`; };
The differences:
classbodies are always strict mode.- Class methods are non-enumerable by default (prototypal versions enumerate by default).
- Classes cannot be called without
new(functions can). superworks in class methods; you'd have to callAnimal.prototype.eat.call(this)manually otherwise.- Class fields (
name = 'rex') are set in the constructor, not on the prototype.
Other than these, the prototype chain is identical. Tooling, debugging, and inspector behavior all look the same.
super — the prototype-walking shortcut #
class Dog extends Animal {
eat() {
return `${super.eat()} (carefully)`; // calls Animal.prototype.eat with this = self
}
}
super.method() finds method by walking up one level on the prototype chain from the current class. The lookup is HOME-relative (the class where the method was defined), not THIS-relative — which is what makes super work correctly through deep inheritance.
Static members live on the constructor, not the prototype #
class Counter {
static count = 0;
static inc() { return ++Counter.count; }
}
Counter.inc(); // 1 — calling on the constructor
new Counter().inc; // undefined — not on instances
The class itself is an object too — it has its own [[Prototype]] pointing at its parent class. Dog.prototype holds instance methods; Dog itself holds static methods.
Reading the prototype chain #
Useful introspection:
Object.getPrototypeOf(obj); // direct prototype
obj.constructor.name; // 'Dog' — the constructor's name
obj instanceof Dog; // walks the chain checking each [[Prototype]]
obj.hasOwnProperty('name'); // true only if 'name' is on `obj` itself, not inherited
Object.hasOwn(obj, 'name'); // ES2022 replacement — safer
Prefer Object.hasOwn because some objects don't inherit from Object.prototype (e.g., Object.create(null)) and don't have hasOwnProperty:
const bareMap = Object.create(null);
bareMap.x = 1;
bareMap.hasOwnProperty('x'); // TypeError
Object.hasOwn(bareMap, 'x'); // true ✓
Property lookup vs property assignment #
Reading walks the chain. Writing does not. Assignment creates an own property on the receiver, shadowing any inherited one:
const parent = { count: 0 };
const child = Object.create(parent);
child.count = 10; // creates OWN property on child
parent.count; // still 0
child.count; // 10 — own wins
Object.getPrototypeOf(child).count; // 0 — parent unchanged
This is how methods on the prototype call instance properties via this — they read fields through the prototype chain to this.
Prototype chain performance #
V8 builds hidden classes (sometimes called shapes) for objects based on their property layout. Objects with the same shape share an optimized JIT path. Things that hurt:
- Adding properties in different orders to objects you treat the same → different shapes.
- Deleting properties → shape transition, often deoptimized.
- Mutating prototypes after instance creation → invalidates inline caches.
- Very deep chains (10+ levels) → slower lookups (mitigated by inline caches, but extremes hurt).
Keep prototype chains shallow. Avoid Object.setPrototypeOf on existing objects. Initialize all properties in the constructor in the same order.
Common gotchas #
Gotcha 1: Forgetting super(...) in derived constructors.
class A {}
class B extends A {
constructor() { /* no super() */ this.x = 1; } // ReferenceError
}
Derived classes must call super() before using this.
Gotcha 2: Arrow functions on class cannot use super.
class A {
doThing = () => super.doThing(); // SyntaxError
}
Arrow fields don't have a method home → no super.
Gotcha 3: Modifying built-in prototypes pollutes everyone.
Array.prototype.sum = function () { return this.reduce((a, b) => a + b); }; // ✗ leaks into every array everywhere
Never modify built-ins outside polyfills.
Gotcha 4: instanceof works across the entire chain, not just direct parents.
rex instanceof Dog; // true
rex instanceof Animal; // true — walks the chain
rex instanceof Object; // true
Recap #
- Every object has a
[[Prototype]]pointing to another object ornull. - The prototype chain is the path the engine walks during property lookup.
classis syntactic sugar over constructor function + prototype.- Instance methods live on
Constructor.prototype; static methods on the constructor itself. - Assignment creates own properties; it does NOT modify the prototype (shadowing).
- Use
Object.create,Object.getPrototypeOf,Object.hasOwn— avoid__proto__andhasOwnProperty. - Don't mutate prototypes after creation, don't modify built-ins, don't go too deep.
Next up: Inheritance Patterns — classes vs composition vs mixins, when each one is the right tool, and why "favor composition over inheritance" is gospel for a reason.
Up next in JavaScript
More from this topic
Enjoyed this article?
Get new JavaScript tutorials delivered. No spam — just code-first articles when they ship.


