JavaScript Prototypes & Inheritance
JavaScript uses a prototype-based inheritance model that's different from class-based inheritance in languages like Java or C++. Understanding prototypes is essential for mastering JavaScript's object-oriented programming capabilities.
What are Prototypes?
In JavaScript, every object has a hidden property called Prototype
(accessible via __proto__
or Object.getPrototypeOf()
), which points to another object called its "prototype". When you try to access a property that doesn't exist on an object, JavaScript automatically looks for it in the object's prototype, then in the prototype's prototype, and so on, forming what's called the "prototype chain".
// Create an object
const animal = {
eats: true,
walk() {
console.log('Animal walking');
}
};
// Create another object with animal as its prototype
const rabbit = Object.create(animal);
rabbit.jumps = true;
// Access properties
console.log(rabbit.jumps); // true (own property)
console.log(rabbit.eats); // true (inherited from animal)
// Call methods
rabbit.walk(); // "Animal walking" (method from animal)
Note: The __proto__
property is deprecated for direct use in code. Use Object.getPrototypeOf()
and Object.setPrototypeOf()
instead.
Constructor Functions and Prototypes
Before ES6 classes, constructor functions were the primary way to create "class-like" functionality in JavaScript. Each constructor function has a prototype
property, which becomes the Prototype
of objects created with that constructor.
// Constructor function
function Animal(name) {
this.name = name;
}
// Adding a method to the prototype
Animal.prototype.speak = function() {
console.log(`${this.name} makes a noise.`);
};
// Create instances
const dog = new Animal('Rex');
const cat = new Animal('Whiskers');
dog.speak(); // "Rex makes a noise."
cat.speak(); // "Whiskers makes a noise."
// All instances share the same method
console.log(dog.speak === cat.speak); // true
The Prototype Chain
When you create an object with a constructor, the object's Prototype
points to the constructor's prototype
property. This creates a chain of inheritance.
// Check the prototype chain
console.log(dog.__proto__ === Animal.prototype); // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null (end of the chain)
Diagram: The prototype chain from a dog instance to Object.prototype
Image generated using Placeholder.com. Free to use for any purpose.
Inheritance with Prototypes
You can create inheritance hierarchies by linking prototypes together.
// Parent constructor
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log(`${this.name} is eating.`);
};
// Child constructor
function Dog(name, breed) {
// Call the parent constructor
Animal.call(this, name);
this.breed = breed;
}
// Set up inheritance
Dog.prototype = Object.create(Animal.prototype);
// Fix the constructor property
Dog.prototype.constructor = Dog;
// Add a method to Dog.prototype
Dog.prototype.bark = function() {
console.log(`${this.name} barks!`);
};
// Create an instance
const rex = new Dog('Rex', 'German Shepherd');
rex.eat(); // "Rex is eating." (inherited from Animal)
rex.bark(); // "Rex barks!" (from Dog)
ES6 Classes and Inheritance
ES6 introduced class syntax, which provides a cleaner way to work with prototypes and inheritance. Under the hood, it still uses the prototype-based inheritance model.
// Parent class
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name} is eating.`);
}
}
// Child class extending Animal
class Dog extends Animal {
constructor(name, breed) {
super(name); // Call parent constructor
this.breed = breed;
}
bark() {
console.log(`${this.name} barks!`);
}
}
// Create an instance
const rex = new Dog('Rex', 'German Shepherd');
rex.eat(); // "Rex is eating." (inherited from Animal)
rex.bark(); // "Rex barks!" (from Dog)
Tip: ES6 classes are just syntactic sugar over JavaScript's prototype-based inheritance. Understanding prototypes helps you understand what's happening under the hood with classes.
Static Methods and Properties
Static methods and properties belong to the constructor/class itself, not to instances.
// Using constructor functions
function Animal(name) {
this.name = name;
}
// Instance method
Animal.prototype.eat = function() {
console.log(`${this.name} is eating.`);
};
// Static method
Animal.compare = function(a, b) {
return a.name === b.name;
};
// Using ES6 classes
class Pet {
constructor(name) {
this.name = name;
}
// Instance method
feed() {
console.log(`Feeding ${this.name}`);
}
// Static method
static create(name) {
return new Pet(name);
}
}
const fluffy = Pet.create('Fluffy');
fluffy.feed(); // "Feeding Fluffy"
Object.create() vs Constructor Functions vs Classes
JavaScript offers multiple ways to create objects and implement inheritance. Here's a comparison:
// 1. Using Object.create()
const animalProto = {
eat() {
console.log(`${this.name} is eating.`);
}
};
const dog = Object.create(animalProto);
dog.name = 'Rex';
dog.bark = function() {
console.log(`${this.name} barks!`);
};
// 2. Using Constructor Functions
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log(`${this.name} is eating.`);
};
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
console.log(`${this.name} barks!`);
};
const rex = new Dog('Rex', 'German Shepherd');
// 3. Using ES6 Classes
class AnimalClass {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name} is eating.`);
}
}
class DogClass extends AnimalClass {
constructor(name, breed) {
super(name);
this.breed = breed;
}
bark() {
console.log(`${this.name} barks!`);
}
}
const buddy = new DogClass('Buddy', 'Golden Retriever');
Common Prototype Patterns
Prototype Delegation
This pattern uses Object.create() to delegate behavior to a prototype object.
// Task prototype with shared methods
const taskPrototype = {
complete() {
this.completed = true;
console.log(`Completed: ${this.description}`);
},
toString() {
return `${this.description} - ${this.completed ? 'Completed' : 'Pending'}`;
}
};
// Factory function to create tasks
function createTask(description) {
return Object.create(taskPrototype, {
description: { value: description, writable: true },
completed: { value: false, writable: true }
});
}
const task1 = createTask('Learn prototypes');
const task2 = createTask('Master JavaScript');
task1.complete(); // "Completed: Learn prototypes"
console.log(task1.toString()); // "Learn prototypes - Completed"
console.log(task2.toString()); // "Master JavaScript - Pending"
Mixins
Mixins allow you to compose objects by copying properties from multiple sources.
// Mixin objects with reusable functionality
const swimmer = {
swim() {
console.log(`${this.name} is swimming.`);
}
};
const flyer = {
fly() {
console.log(`${this.name} is flying.`);
}
};
// Base class
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name} is eating.`);
}
}
// Create a Duck class that uses both mixins
class Duck extends Animal {
constructor(name) {
super(name);
}
}
// Apply mixins
Object.assign(Duck.prototype, swimmer, flyer);
const donald = new Duck('Donald');
donald.eat(); // "Donald is eating."
donald.swim(); // "Donald is swimming."
donald.fly(); // "Donald is flying."
Prototype Pitfalls and Best Practices
Modifying Built-in Prototypes
Modifying built-in prototypes like Array.prototype or Object.prototype is generally considered a bad practice.
Warning: Extending built-in prototypes can lead to naming conflicts, compatibility issues, and unexpected behavior in third-party libraries.
// Bad practice
Array.prototype.first = function() {
return this[0];
};
// Better approach: Create a utility function
function getFirst(array) {
return array[0];
}
// Or use a custom class that extends Array
class MyArray extends Array {
first() {
return this[0];
}
}
Property Shadowing
When an object has a property with the same name as a property in its prototype chain, the object's own property "shadows" the prototype property.
const animal = {
speak() {
console.log('Animal sound');
}
};
const dog = Object.create(animal);
// This shadows the speak method from the prototype
dog.speak = function() {
console.log('Woof!');
};
dog.speak(); // "Woof!"
// To call the prototype method
animal.speak.call(dog); // "Animal sound"
Interactive Demo
Try out this interactive example to see prototypes and inheritance in action:
Create a Vehicle
Create a Car (inherits from Vehicle)
Object Information:
Create an object to see its properties and prototype chain.
Next Steps
Now that you understand JavaScript prototypes and inheritance, you can explore related topics:
- Object-Oriented Design Patterns in JavaScript
- ES6+ Class Features (getters, setters, private fields)
- Composition vs. Inheritance
- Functional Programming in JavaScript
- TypeScript and its class-based approach