JavaScript Prototypes and Inheritance: How Objects Really Work
Understand the prototype chain, how inheritance works under the hood, and the relationship between constructors, prototypes, and instances.
JavaScript's Inheritance Model
Unlike classical OOP languages (Java, C++), JavaScript uses prototypal inheritance. Objects inherit directly from other objects, not from classes.
Even with ES6 classes, prototypes are still how it works underneath!
Every Object Has a Prototype
When you access a property on an object, JavaScript:
1. Looks on the object itself
2. If not found, looks on its prototype
3. If not found, looks on the prototype's prototype
4. Continues until reaching null
This is the prototype chain.
``javascript
const animal = {
eats: true,
walk() {
console.log('Walking...');
}
};
const rabbit = { jumps: true, __proto__: animal // rabbit's prototype is animal };
console.log(rabbit.jumps); // true (own property)
console.log(rabbit.eats); // true (inherited from animal)
rabbit.walk(); // "Walking..." (inherited method)
`
The Prototype Property
Functions have a prototype property used when creating objects with new.
`javascript
function User(name) {
this.name = name;
}
// Methods go on the prototype (shared by all instances) User.prototype.sayHello = function() { console.log('Hello, ' + this.name); };
const alice = new User('Alice'); const bob = new User('Bob');
alice.sayHello(); // "Hello, Alice" bob.sayHello(); // "Hello, Bob"
// Both share the same method console.log(alice.sayHello === bob.sayHello); // true
// The prototype chain
console.log(alice.__proto__ === User.prototype); // true
console.log(User.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null (end of chain)
`
Understanding the Relationships
`
+----------------+
| User | (Constructor Function)
|----------------|
| prototype -----+---> User.prototype
+----------------+ +------------------+
| | constructor ----->|
| | sayHello() |
| | __proto__ -------->Object.prototype
| +------------------+
| ^
v |
+----------------+ |
new User | alice | |
|----------------| |
| name: 'Alice' | |
| __proto__ -----+------------+
+----------------+
`
Prototype Methods
Object.create()
Create objects with a specific prototype:
`javascript
const animal = {
speak() {
console.log(this.sound);
}
};
const dog = Object.create(animal); dog.sound = 'Woof!'; dog.speak(); // "Woof!"
// dog's prototype is animal
console.log(Object.getPrototypeOf(dog) === animal); // true
`
Object.getPrototypeOf() / Object.setPrototypeOf()
`javascript
const proto = { greet() { return 'Hi!'; } };
const obj = {};
Object.setPrototypeOf(obj, proto);
console.log(obj.greet()); // "Hi!"
console.log(Object.getPrototypeOf(obj) === proto); // true
`
hasOwnProperty()
Check if property exists on object itself (not inherited):
`javascript
const animal = { eats: true };
const rabbit = Object.create(animal);
rabbit.jumps = true;
console.log(rabbit.hasOwnProperty('jumps')); // true (own)
console.log(rabbit.hasOwnProperty('eats')); // false (inherited)
console.log('eats' in rabbit); // true (including inherited)
`
ES6 Classes: Syntactic Sugar
Classes are just cleaner syntax over prototypes:
`javascript
class Animal {
constructor(name) {
this.name = name;
}
speak() { console.log(this.name + ' makes a sound'); } }
class Dog extends Animal { constructor(name, breed) { super(name); // Call parent constructor this.breed = breed; }
speak() { console.log(this.name + ' barks'); }
fetch() { console.log(this.name + ' fetches the ball'); } }
const rex = new Dog('Rex', 'German Shepherd'); rex.speak(); // "Rex barks" rex.fetch(); // "Rex fetches the ball"
// Under the hood, it's still prototypes!
console.log(rex.__proto__ === Dog.prototype); // true
console.log(Dog.prototype.__proto__ === Animal.prototype); // true
`
Inheritance Patterns
Classical Pattern (ES6 Classes)
`javascript
class Vehicle {
constructor(wheels) {
this.wheels = wheels;
}
drive() { console.log('Driving on ' + this.wheels + ' wheels'); } }
class Car extends Vehicle {
constructor() {
super(4);
}
}
`
Object Composition (Often Preferred)
`javascript
// Instead of inheritance, compose behaviors
const canWalk = {
walk() { console.log('Walking...'); }
};
const canSwim = { swim() { console.log('Swimming...'); } };
const canFly = { fly() { console.log('Flying...'); } };
// Create duck with multiple abilities
const duck = Object.assign({}, canWalk, canSwim, canFly);
duck.walk(); // "Walking..."
duck.swim(); // "Swimming..."
duck.fly(); // "Flying..."
`
Common Mistakes
Mistake 1: Overwriting the Prototype
`javascript
function User(name) {
this.name = name;
}
// BAD: Replaces entire prototype, loses constructor User.prototype = { sayHello() { console.log('Hello'); } };
// GOOD: Add to existing prototype
User.prototype.sayHello = function() {
console.log('Hello');
};
`
Mistake 2: Arrow Functions in Prototypes
`javascript
function User(name) {
this.name = name;
}
// BAD: Arrow function doesn't have its own 'this' User.prototype.greet = () => { console.log(this.name); // 'this' is not the instance! };
// GOOD: Regular function
User.prototype.greet = function() {
console.log(this.name); // 'this' is the instance
};
`
Summary
- JavaScript uses prototypal inheritance (objects inherit from objects)
- Every object has a prototype (accessible via __proto__ or Object.getPrototypeOf)
- Constructor functions have a prototype` property used for new instances
- ES6 classes are syntactic sugar over prototype-based inheritance
- Prototype chain: object -> prototype -> prototype's prototype -> ... -> null
- Prefer composition over inheritance when possible