Prototypes and prototypal inheritance in JavaScript
Prototypal inheritance in JavaScript means objects delegate property lookups to other objects through a chain of live links, rather than copying behavior at creation time.
Theory
TL;DR
- Analogy: you ask your dad for his pasta recipe. If he doesn't remember, he asks his dad. The chain ends at
null. - Main difference from classical OOP: delegation at runtime, not copying at creation.
- V8 stores
[[Prototype]]as a hidden pointer and walks the chain on every property miss. Object.create()for explicit delegation, classes for readable hierarchies (they use the same mechanism under the hood).- Chains deeper than 10 levels slow lookups noticeably in V8. Keep it shallow.
Quick example
const parent = { greeting: 'Hi' };
const child = Object.create(parent); // child's [[Prototype]] = parent
console.log(child.greeting); // 'Hi' — delegated up the chain
child.surname = 'Smith';
console.log(child.surname); // 'Smith' — own property
console.log(parent.surname); // undefined — parent was not mutatedchild has no greeting of its own, so JS walks up to parent and finds it there. Setting child.surname creates an own property on child, not on parent. The parent stays untouched.
Key difference from classical inheritance
Classical languages like Java copy parent behavior into each child at creation. The child is self-contained from that point forward. JavaScript does the opposite: nothing is copied. The child holds a live reference to its parent object, and the engine walks that reference at runtime on every lookup. Changes to the parent propagate to all children instantly, unless a child has shadowed that property with its own value.
When to use
- Sharing methods across many instances (like
Array.prototype.map) — prototypes. - Fixed hierarchies with clear structure and private state — classes (they use prototypes internally anyway).
- Dynamic extension at runtime for plugins or mixins —
Object.create(). - Performance-critical paths — avoid chains deeper than 5 levels as a safe limit. V8 documents degraded performance at 10+ levels.
How V8 handles it
V8 stores [[Prototype]] as a hidden pointer on every object. When you access a property, V8 runs GetProperty, checks own keys first via inline caches (LoadIC), and on a miss follows the pointer to the next object. This repeats until null, which returns undefined.
Two things hurt performance here. First, calling Object.setPrototypeOf() on an existing object invalidates V8's inline caches, forcing a re-optimization pass. Do it once at creation, not in loops. Second, chains deeper than 10 objects push inline caches into a megamorphic state, making lookups roughly 5-10x slower per the V8 blog.
Common mistakes
Mutating a shared prototype and expecting instance isolation.
const proto = { count: 0 };
const a = Object.create(proto);
const b = Object.create(proto);
proto.count++; // both a and b see this change
console.log(a.count, b.count); // 1 1The prototype is shared by reference, not copied. If each instance needs its own value, set it directly on the instance: a.count = 0.
for...in iterates the entire chain.
const obj = Object.create({ inherited: 'surprise' });
obj.own = 'mine';
for (let key in obj) console.log(key); // 'own', then 'inherited'Use Object.keys() for own properties only, or add Object.prototype.hasOwnProperty.call(obj, key) inside the loop.
Calling Object.setPrototypeOf() in a loop.
let obj = {};
for (let i = 0; i < 100; i++) {
Object.setPrototypeOf(obj, {}); // invalidates V8 caches on every iteration
}Build the prototype chain once with Object.create(). Mutating it repeatedly is a V8 deoptimization trap.
Assuming instanceof checks only one level.
const plain = Object.create(null); // no Object.prototype at all
console.log(plain instanceof Object); // falseObject.create(null) produces a pure dictionary with no chain. Useful for hashmaps without prototype pollution risk, but instanceof, toString(), and similar methods will not work on it.
Real-world usage
- React: synthetic events delegate to a shared prototype for pool reuse (ReactDOMSyntheticEvent).
- Node.js:
http.Serverinherits fromnet.Servervia the prototype chain, which is how EventEmitter methods reach HTTP handlers. - Express: middleware delegation for shared validators and auth checks.
- Vue.js: component instances share a common options prototype.
- Every array delegates
.map(),.filter(),.reduce()fromArray.prototype.
Follow-up questions
Q: What is the output?
const a = {};
const b = Object.create(a);
a.x = 1;
delete a.x;
console.log(b.x);
A: undefined. After delete a.x the property is gone from a. b delegates to a, finds nothing, returns undefined.
Q: What is the difference between __proto__ and [[Prototype]]?
A: [[Prototype]] is the internal slot defined in the spec. __proto__ is a getter/setter on Object.prototype that exposes it. It is non-standard in strict environments and deprecated in production code. Use Object.getPrototypeOf() instead.
Q: How do ES6 classes relate to prototypes?
A: Classes are syntax sugar over the same mechanism. class Person { greet() {} } creates a constructor function and writes greet to Person.prototype. Object.getPrototypeOf(new Person()) === Person.prototype is always true.
Q: What is Object.create(null) useful for?
A: It creates an object with no prototype chain at all. No inherited toString, no hasOwnProperty, no prototype keys. That makes it a clean dictionary, safe for storing arbitrary user-supplied keys without prototype pollution risk.
Q: (Senior) V8 optimizes shallow chains. At what depth does it deoptimize, and what is the practical cost?
A: Around 10-15 levels triggers megamorphic inline cache states. The V8 blog documents roughly 5x slower lookups on a 20-level chain compared to a 2-level one. If you are building a plugin system with dynamic Object.create() chains, this is worth benchmarking with your actual data.
Examples
Basic prototype chain delegation
const vehicle = {
type: 'vehicle',
describe() {
console.log(`This is a ${this.type}`);
}
};
const car = Object.create(vehicle);
car.type = 'car'; // shadows vehicle.type
const sportsCar = Object.create(car);
// sportsCar has no type of its own, delegates to car
sportsCar.describe(); // 'This is a car'
console.log(Object.getPrototypeOf(sportsCar) === car); // true
console.log(Object.getPrototypeOf(car) === vehicle); // truesportsCar has no describe, so the engine walks up to car. car has none either, so it continues to vehicle where it finds the method. Inside that call, this.type resolves to car.type because sportsCar delegates to car, which has that property set as its own.
Constructor functions and shared methods
This is how shared methods worked before ES6 classes. It is also the exact mechanism classes use internally.
function User(name, role) {
this.name = name;
this.role = role;
}
User.prototype.canEdit = function() {
return this.role === 'admin';
};
User.prototype.greet = function() {
console.log(`Hi, I am ${this.name}`);
};
const alice = new User('Alice', 'admin');
const bob = new User('Bob', 'viewer');
alice.greet(); // 'Hi, I am Alice'
console.log(alice.canEdit()); // true
console.log(bob.canEdit()); // false
// Both share the same function references
console.log(alice.canEdit === bob.canEdit); // truenew User(...) creates a plain object and sets its [[Prototype]] to User.prototype. The functions live once on that object, not duplicated per instance. That is the memory-efficiency argument for prototypes: 1000 instances share one copy of each method.
Prototype mutation surprise in production
I have seen this bug in real codebases when developers mix shared prototype state with per-instance logic.
const SyntheticEventProto = {
bubbles: false,
preventDefault() {
this.defaultPrevented = true;
}
};
const clickEvent = Object.create(SyntheticEventProto);
clickEvent.type = 'click';
clickEvent.preventDefault();
console.log(clickEvent.defaultPrevented); // true
const mouseEvent = Object.create(SyntheticEventProto);
// Someone mutates the shared prototype directly
SyntheticEventProto.bubbles = true;
console.log(clickEvent.bubbles); // true — shared surprise
console.log(mouseEvent.bubbles); // true — same prototype, same changeclickEvent.defaultPrevented is safe because preventDefault() sets this.defaultPrevented, which creates an own property on the instance. But mutating SyntheticEventProto.bubbles directly changes the shared object. Every instance that has not shadowed bubbles with its own value now sees true. React's actual implementation (ReactDOMSyntheticEvent) uses similar prototype pooling, which is why reading event properties asynchronously after React 16 could return unexpected values.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.