JavaScript Prototype Chain Explained: Inheritance Without Classes
JavaScript's inheritance model is fundamentally different from classical OOP languages like Java. Understanding prototypes unlocks how JavaScript actually works — and explains a lot of "surprising" behavior.
Every Object Has a Prototype
const dog = { name: 'Rex', sound: 'Woof' };
// dog's prototype is Object.prototype
console.log(Object.getPrototypeOf(dog) === Object.prototype); // true
// Object.prototype's prototype is null (end of chain)
console.log(Object.getPrototypeOf(Object.prototype)); // null
The Prototype Chain
When you access a property, JavaScript walks up the chain:
const animal = {
breathe() { return 'breathing...'; },
eat() { return 'eating...'; }
};
const dog = {
bark() { return 'Woof!'; }
};
// Set dog's prototype to animal
Object.setPrototypeOf(dog, animal);
// dog can now use animal's methods
console.log(dog.bark()); // 'Woof!' (own property)
console.log(dog.breathe()); // 'breathing...' (from prototype)
console.log(dog.eat()); // 'eating...' (from prototype)
// Property lookup chain:
// dog.breathe → not found on dog → check dog's prototype (animal) → found!
__proto__ vs prototype
This confuses everyone. Here's the clear distinction:
// __proto__ — the actual prototype link on every object instance
const obj = {};
console.log(obj.__proto__ === Object.prototype); // true
// .prototype — exists only on FUNCTIONS, used as template for new instances
function Dog(name) {
this.name = name;
}
Dog.prototype.bark = function() { return 'Woof!'; };
const rex = new Dog('Rex');
console.log(rex.__proto__ === Dog.prototype); // true ← key relationship!
Dog (constructor function)
│
│ .prototype
↓
Dog.prototype ←─── rex.__proto__
{ bark: fn } (same object)
│
│ __proto__
↓
Object.prototype
{ toString, hasOwnProperty, ... }
│
│ __proto__
↓
null
The new Keyword
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {
return `Hi, I'm ${this.name}`;
};
const alice = new Person('Alice', 30);
// new does 4 things:
// 1. Creates empty object: {}
// 2. Sets __proto__: {}.__proto__ = Person.prototype
// 3. Calls Person with this = the new object
// 4. Returns the object (unless constructor returns different object)
console.log(alice.name); // 'Alice' (own property)
console.log(alice.greet()); // 'Hi, I'm Alice' (from prototype)
console.log(alice instanceof Person); // true
Class Syntax: Sugar Over Prototypes
ES6 classes are syntactic sugar — they use the same prototype system underneath.
// ES5 prototype-based
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
return `${this.name} makes a noise.`;
};
function Dog(name) {
Animal.call(this, name); // Call parent constructor
}
Dog.prototype = Object.create(Animal.prototype); // Set up inheritance
Dog.prototype.constructor = Dog; // Fix constructor reference
Dog.prototype.bark = function() {
return `${this.name} barks.`;
};
// ES6 class (identical behavior)
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a noise.`;
}
}
class Dog extends Animal {
bark() {
return `${this.name} barks.`;
}
}
const d = new Dog('Rex');
console.log(d.speak()); // Rex makes a noise. (from Animal.prototype)
console.log(d.bark()); // Rex barks. (from Dog.prototype)
Object.create() — Explicit Prototype Inheritance
const vehicleProto = {
start() { return `${this.model} starting...`; },
stop() { return `${this.model} stopping.`; },
};
// Create object with vehicleProto as its prototype
const car = Object.create(vehicleProto);
car.model = 'Tesla Model 3';
car.drive = function() { return `Driving ${this.model}`; };
console.log(car.start()); // Tesla Model 3 starting... (from proto)
console.log(car.drive()); // Driving Tesla Model 3 (own method)
// Object.create(null) — no prototype (useful for pure dictionaries)
const pureDict = Object.create(null);
pureDict.key = 'value';
// No toString, no hasOwnProperty — truly clean object
Checking Own vs Inherited Properties
class Animal {
constructor(name) { this.name = name; }
speak() { return 'some noise'; }
}
class Dog extends Animal {
bark() { return 'Woof!'; }
}
const rex = new Dog('Rex');
// hasOwnProperty — only own (not inherited) properties
console.log(rex.hasOwnProperty('name')); // true (set in constructor)
console.log(rex.hasOwnProperty('bark')); // false (on Dog.prototype)
console.log(rex.hasOwnProperty('speak')); // false (on Animal.prototype)
// in operator — checks entire chain
console.log('name' in rex); // true
console.log('bark' in rex); // true
console.log('speak' in rex); // true
console.log('fly' in rex); // false
// instanceof — checks prototype chain
console.log(rex instanceof Dog); // true
console.log(rex instanceof Animal); // true
console.log(rex instanceof Object); // true (everything is!)
Mixin Pattern (Composing Behaviors)
// Mixins let you compose behaviors without deep inheritance chains
const Serializable = {
serialize() {
return JSON.stringify(this);
},
static deserialize(json) {
return Object.assign(new this(), JSON.parse(json));
}
};
const Validatable = {
validate() {
return Object.keys(this).every(key => this[key] !== null);
}
};
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
// Mix in behaviors
Object.assign(User.prototype, Serializable, Validatable);
const user = new User('Alice', 'alice@example.com');
console.log(user.serialize()); // '{"name":"Alice","email":"alice@example.com"}'
console.log(user.validate()); // true
Common Gotchas
// ❌ Gotcha: Shared mutable state on prototype
function Team() {}
Team.prototype.members = []; // Shared across ALL instances!
const t1 = new Team();
const t2 = new Team();
t1.members.push('Alice');
console.log(t2.members); // ['Alice'] — oops!
// ✅ Fix: Initialize in constructor
function Team() {
this.members = []; // Own property per instance
}
// ❌ Gotcha: Arrow functions can't be constructors
const Foo = () => {};
new Foo(); // TypeError: Foo is not a constructor
// ❌ Gotcha: Arrow functions don't have their own prototype
const bar = () => {};
console.log(bar.prototype); // undefined
Summary
- Every JS object has a prototype (except
Object.create(null)) - Property lookup walks up the chain until found or reaches
null __proto__: actual prototype link on instances.prototype: exists on functions, becomes__proto__ofnewinstances- ES6 classes are syntax sugar — same prototype system underneath
- Prefer composition (mixins) over deep inheritance hierarchies
→ Inspect nested JSON/object structures with the JSON Viewer.