Prototypal inheritance is JavaScript's core object system. Learn prototype chains, Object.create, and how class syntax maps to prototypes.
Picture a company employee handbook. Every department has its own handbook, but instead of duplicating the company-wide rules in each department's version, the department handbook just says "for anything not in here, see the company handbook." If the company handbook doesn't have it either, it says "see the legal document on file with HR." Each handbook delegates up a chain rather than repeating what's already written above. That chain is the prototype chain. Every JavaScript object has a hidden link to another object — its prototype. When you access a property the object doesn't have directly, JavaScript follows the link to the prototype and looks there. If it's not there either, it follows that object's link, and so on, until it either finds the property or reaches the end of the chain (null) and returns undefined. The key insight: prototypes are not a way to copy behavior — they're a live delegation chain. Objects don't get private copies of their prototype's methods. They share them. When you call array.push(), there's no push method on your specific array — it's on Array.prototype, and your array delegates there at call time. This is how JavaScript achieves inheritance without classes — though classes are now available as syntax on top of exactly this same system.
In JavaScript, almost every object has an internal slot called [[Prototype]] — a link to another object. Property lookup follows this chain automatically and silently.
const obj = { name: 'Alice' }
// obj's [[Prototype]] is Object.prototype
// You can access it via:
Object.getPrototypeOf(obj) // Object.prototype — the right way
obj.__proto__ // Object.prototype — works but not recommended
// Object.prototype's [[Prototype]] is null — end of the chain
Object.getPrototypeOf(Object.prototype) // null
// Property lookup in action:
console.log(obj.name) // 'Alice' — found on obj directly
console.log(obj.toString()) // works — not on obj, found on Object.prototype
console.log(obj.missing) // undefined — not found anywhere in the chain
const animal = {
breathes: true,
describe() { return `I breathe: ${this.breathes}` }
}
const dog = {
name: 'Rex',
bark() { return 'Woof!' }
}
// Set dog's prototype to animal
Object.setPrototypeOf(dog, animal)
// Now the chain is: dog → animal → Object.prototype → null
dog.name // 'Rex' — found on dog
dog.bark() // 'Woof!' — found on dog
dog.breathes // true — NOT on dog, found on animal (prototype)
dog.describe() // 'I breathe: true' — found on animal, but THIS = dog
// 'this' in prototype methods refers to the calling object, not the prototype
dog.missing // undefined — not on dog, not on animal, not on Object.prototype
dog.hasOwnProperty('name') // true — 'name' is OWN property
dog.hasOwnProperty('breathes') // false — 'breathes' is inherited
Before ES6 classes, constructor functions were the standard way to create objects with shared methods. Understanding them reveals exactly what classes do under the hood.
// Constructor function — called with 'new'
function Person(name, age) {
// 'this' is the newly created object
this.name = name // OWN property — stored on each instance
this.age = age // OWN property — stored on each instance
// Don't do this — creates a new function for each instance:
// this.greet = function() { ... }
}
// Methods go on the PROTOTYPE — shared by all instances
Person.prototype.greet = function() {
return `Hi, I'm ${this.name}, age ${this.age}`
}
Person.prototype.birthday = function() {
this.age++
}
const alice = new Person('Alice', 30)
const bob = new Person('Bob', 25)
alice.greet() // "Hi, I'm Alice, age 30"
bob.greet() // "Hi, I'm Bob, age 25"
// Both instances share ONE greet function on Person.prototype — not copies
alice.greet === bob.greet // true — same function reference
// The prototype chain for alice:
// alice → Person.prototype → Object.prototype → null
// When you call new Person('Alice', 30), JavaScript:
// 1. Creates a new empty object: {}
// 2. Sets its [[Prototype]] to Person.prototype
// 3. Calls Person with 'this' set to the new object
// 4. Returns the new object (unless Person explicitly returns a different object)
// Manual implementation of new:
function myNew(Constructor, ...args) {
const instance = Object.create(Constructor.prototype) // steps 1 & 2
const result = Constructor.apply(instance, args) // step 3
return typeof result === 'object' && result !== null // step 4
? result
: instance
}
const alice = myNew(Person, 'Alice', 30)
alice.greet() // "Hi, I'm Alice, age 30" — works identically
// Object.create(proto) creates a new object with proto as its [[Prototype]]
const animal = {
type: 'Animal',
describe() { return `I am a ${this.type} named ${this.name}` }
}
const dog = Object.create(animal)
dog.type = 'Dog'
dog.name = 'Rex'
dog.bark = function() { return 'Woof!' }
dog.describe() // 'I am a Dog named Rex' — uses animal's method, dog's data
dog.bark() // 'Woof!'
// Object.create(null) — object with NO prototype
const pure = Object.create(null)
pure.key = 'value'
pure.toString // undefined — no Object.prototype in chain
// Safe as a dictionary — no inherited properties that could clash with data keys
Classes don't add a new object model. They're a cleaner syntax for the same constructor + prototype pattern.
// ES6 class
class Animal {
constructor(name, sound) {
this.name = name // own property on each instance
this.sound = sound // own property on each instance
}
speak() { // goes on Animal.prototype
return `${this.name} says ${this.sound}`
}
static create(name, sound) { // static — on Animal itself, not Animal.prototype
return new Animal(name, sound)
}
}
class Dog extends Animal {
constructor(name) {
super(name, 'Woof') // calls Animal's constructor
this.tricks = [] // own property
}
learn(trick) { // goes on Dog.prototype
this.tricks.push(trick)
}
speak() { // OVERRIDES Animal.prototype.speak on Dog.prototype
return `${super.speak()} and knows ${this.tricks.length} tricks`
}
}
const rex = new Dog('Rex')
rex.learn('sit')
rex.speak() // 'Rex says Woof and knows 1 tricks'
// Prototype chain for rex:
// rex → Dog.prototype → Animal.prototype → Object.prototype → null
// Exactly equivalent to the pre-class version:
Object.getPrototypeOf(rex) // Dog.prototype
Object.getPrototypeOf(Dog.prototype) // Animal.prototype
Object.getPrototypeOf(Animal.prototype) // Object.prototype
const parent = { inherited: true }
const child = Object.create(parent)
child.own = true
'own' in child // true — checks entire chain
'inherited' in child // true — found on parent via chain
'missing' in child // false — not anywhere
child.hasOwnProperty('own') // true — directly on child
child.hasOwnProperty('inherited') // false — on parent, not child
// for...in iterates ALL enumerable properties including inherited ones
for (const key in child) {
console.log(key) // 'own', 'inherited'
}
// Object.keys — own enumerable only
Object.keys(child) // ['own'] — ignores inherited
// Object.prototype is shared by ALL objects
// Modifying it affects every object in your program — and every library
// ❌ Never do this:
Object.prototype.isAdmin = true
const user = {}
user.isAdmin // true — now every object has isAdmin!
// This is prototype pollution — a real attack vector in security exploits
// User-controlled input like { '__proto__': { 'isAdmin': true } }
// when merged with a target object can inject properties on Object.prototype
// Safe merge patterns:
function safeMerge(target, source) {
for (const key of Object.keys(source)) {
if (key !== '__proto__' && key !== 'constructor' && key !== 'prototype') {
target[key] = source[key]
}
}
return target
}
// Or use Object.assign on a null-prototype object for config parsing:
const config = Object.assign(Object.create(null), userInput)
// Property lookup traverses the chain on every access
// Deeply nested prototype chains are slower (though JS engines optimize heavily)
// Shallow is faster — keep inheritance chains short (3-4 levels max)
// Own properties are fastest — direct access, no chain traversal
// V8 optimization: hidden classes
// V8 creates a hidden class for each unique property layout
// Adding properties in a consistent order allows V8 to optimize property access
// Breaking the pattern (adding props in random order or deleting them) degrades performance
// ✓ Consistent property layout:
function Point(x, y) {
this.x = x // always added in this order
this.y = y // V8 can optimize
}
// ❌ Inconsistent layout:
const p1 = {}; p1.x = 1; p1.y = 2
const p2 = {}; p2.y = 1; p2.x = 2 // different order — different hidden classMany devs think ES6 classes create a fundamentally different object model than prototypes — but actually classes are syntactic sugar over constructor functions and prototype chains. The runtime behavior is identical. class Foo {} creates a function Foo, and all methods go on Foo.prototype, linked to every instance via [[Prototype]], exactly as they did before classes existed.
Many devs think prototype methods are copied onto each new instance — but actually instances never get copies of prototype methods. They share a single copy that lives on the prototype object. This is the memory efficiency of prototypal inheritance — 1000 Array instances share one Array.prototype.push, not 1000 separate push functions.
Many devs think __proto__ is the right way to access the prototype — but actually __proto__ is a deprecated accessor that works in browsers for historical reasons but is not part of the core specification. The correct API is Object.getPrototypeOf(obj) to read and Object.setPrototypeOf(obj, proto) to set. __proto__ works but signals unfamiliarity with modern JavaScript.
Many devs think Object.create(null) creates an object with no useful properties — but actually it creates an object with no prototype at all, making it a pure dictionary with zero risk of property name collisions with inherited properties like toString, hasOwnProperty, or valueOf. This is the correct tool for safe key-value stores built from user input.
Many devs think instanceof checks if an object is a direct instance of a class — but actually instanceof walks the entire prototype chain. new Dog() instanceof Animal is true if Animal.prototype appears anywhere in Dog's prototype chain. It does not check if the object was directly constructed by that constructor — only if the prototype is in the chain.
Many devs think modifying a prototype is always harmless — but actually prototype pollution (adding or modifying properties on Object.prototype or other shared prototypes) affects every object in the application and every library. It is a real security vulnerability class with CVEs, not just a style concern, and user input that merges into objects without sanitization is the most common attack vector.
All built-in JavaScript methods exist on prototypes — Array.prototype has push, pop, map, filter, reduce. String.prototype has split, trim, includes. You never construct these methods yourself, yet every array and string you create has access to them through the prototype chain. The language itself is built on prototype delegation.
Polyfills work by adding methods to built-in prototypes — if older browsers don't have Array.prototype.includes, a polyfill adds it: if (!Array.prototype.includes) { Array.prototype.includes = function() { ... } }. This is prototype extension used correctly (on non-existent methods, in controlled environments). It's the exact same mechanism that makes prototype pollution dangerous when done with user input.
React's class components are JavaScript classes (prototype-based) that extend React.Component — meaning every class component instance gets React's lifecycle methods (componentDidMount, render, setState) through prototype chain delegation. Function components with hooks replaced this pattern entirely because prototype chains added cognitive overhead that hooks eliminated.
Lodash's chain() method temporarily modifies an object's prototype to enable method chaining — _.chain(array).filter().map().value() works because chain() creates a wrapper object whose prototype has all Lodash methods. This is prototype-based API design where the prototype is set programmatically for a specific use case.
Node.js's EventEmitter is pure prototype-based inheritance — EventEmitter.prototype.on, EventEmitter.prototype.emit, EventEmitter.prototype.removeListener are all defined on the prototype. When you extend EventEmitter in Node.js (class MyEmitter extends EventEmitter), your instances get all event handling through prototype delegation, exactly as prototypes were designed to work.
TypeScript's compiled class output in older targets (ES5) is a manual recreation of the prototype pattern — the TypeScript compiler generates a constructor function with methods on the prototype, a __extends helper that wires up the prototype chain, and calls to the parent constructor via call(). Reading TypeScript's compiled output is the best crash course in what ES6 classes actually do.
How does prototypal inheritance work in JavaScript?
Prototype pollution vulnerability
Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.