Prototype Inheritance: Multiple Inheritance Patterns in JavaScript
Unlike class-based programming languages, JavaScript's inheritance mechanism is built upon links between objects. This mechanism is called prototype inheritance. When you access an object's property, if the object itself doesn't have it, the engine automatically follows this link upward. Understanding this delegation mechanism is key to mastering JavaScript's unique object-oriented features.
Basic Concepts of Prototype Inheritance
Prototype inheritance is a way JavaScript implements inheritance relationships between objects. Through prototype inheritance, an object can access the properties and methods of another object (its prototype). This inheritance approach differs from the class inheritance in traditional object-oriented languages; it's based on delegation relationships between objects.
Basic Prototype Inheritance
// Parent object
const animal = {
type: "animal",
speak() {
console.log(`${this.type} makes a sound`);
},
eat() {
console.log(`${this.type} is eating`);
},
};
// Create object inheriting from animal using Object.create()
const dog = Object.create(animal);
dog.type = "dog";
dog.bark = function () {
console.log("Woof! Woof!");
};
// dog inherits animal's methods
dog.speak(); // dog makes a sound
dog.eat(); // dog is eating
dog.bark(); // Woof! Woof!
// Check prototype relationship
console.log(dog.__proto__ === animal); // truePrototype Chain Inheritance Patterns
1. Simple Prototype Inheritance
This is the most basic inheritance method, directly pointing the child constructor's prototype to an instance of the parent constructor.
function Parent(name) {
this.name = name;
this.colors = ["red", "green", "blue"];
}
Parent.prototype.sayName = function () {
console.log(this.name);
};
function Child(name, age) {
this.age = age;
}
// Key: Point Child's prototype to Parent's instance
Child.prototype = new Parent();
const child1 = new Child("John", 25);
const child2 = new Child("Sarah", 30);
child1.sayName(); // undefined (because Parent constructor was called without passing name)
child1.colors.push("yellow");
// Problem: Reference type properties are shared by all instances
console.log(child2.colors); // ['red', 'green', 'blue', 'yellow']Problem Analysis:
- Reference type properties are shared by all instances
- Cannot pass parameters to parent constructor when creating child instances
2. Constructor Inheritance (Borrowing Constructors)
To solve the problems with prototype inheritance, constructor inheritance pattern emerged.
function Parent(name, colors) {
this.name = name;
this.colors = colors;
this.sayName = function () {
console.log(this.name);
};
}
function Child(name, age, colors) {
// Key: Borrow Parent constructor, use call or apply
Parent.call(this, name, colors);
this.age = age;
}
const child1 = new Child("John", 25, ["red", "green"]);
const child2 = new Child("Sarah", 30, ["blue", "yellow"]);
child1.sayName(); // John
child2.sayName(); // Sarah
// Each has independent colors arrays
child1.colors.push("purple");
console.log(child1.colors); // ['red', 'green', 'purple']
console.log(child2.colors); // ['blue', 'yellow']
// Problem: Methods defined in constructor, cannot achieve method reuse
console.log(child1.sayName === child2.sayName); // falseAdvantages:
- Solves the shared reference type problem
- Can pass parameters to parent constructor
Disadvantages:
- Methods defined in constructor are recreated each time an instance is created
- Cannot achieve function reuse
3. Combined Inheritance (Pseudo-classical Inheritance)
Combined inheritance combines the advantages of prototype chain inheritance and constructor inheritance, making it the most commonly used inheritance pattern.
function Parent(name, colors) {
this.name = name;
this.colors = colors;
}
// Methods defined on prototype
Parent.prototype.sayName = function () {
console.log(this.name);
};
Parent.prototype.sayColors = function () {
console.log(this.colors.join(", "));
};
function Child(name, age, colors) {
// Inherit properties (borrow constructor)
Parent.call(this, name, colors);
this.age = age;
}
// Inherit methods (prototype chain)
Child.prototype = new Parent();
Child.prototype.constructor = Child; // Fix constructor reference
// Add child-specific methods
Child.prototype.sayAge = function () {
console.log(`I am ${this.age} years old`);
};
const child1 = new Child("John", 25, ["red", "green"]);
const child2 = new Child("Sarah", 30, ["blue", "yellow"]);
// Verify inheritance
child1.sayName(); // John
child1.sayColors(); // red, green
child1.sayAge(); // I am 25 years old
// Reference types are independent
child1.colors.push("purple");
console.log(child1.colors); // ['red', 'green', 'purple']
console.log(child2.colors); // ['blue', 'yellow']
// Method reuse
console.log(child1.sayName === child2.sayName); // true
console.log(child1.sayColors === child2.sayColors); // trueImproved Version of Combined Inheritance:
function Parent(name, colors) {
this.name = name;
this.colors = colors;
}
Parent.prototype.sayName = function () {
console.log(this.name);
};
function Child(name, age, colors) {
Parent.call(this, name, colors);
this.age = age;
}
// Use Object.create instead of new Parent()
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
Child.prototype.sayAge = function () {
console.log(`I am ${this.age} years old`);
};This improved version avoids the problem of calling the parent constructor twice.
4. Parasitic Inheritance
Parasitic inheritance creates a function that encapsulates the inheritance process and enhances the object internally.
function createAnother(original) {
// Create a new object inheriting from original
const clone = Object.create(original);
// Enhance the object
clone.sayHi = function () {
console.log("Hi!");
};
return clone;
}
const person = {
name: "John",
friends: ["Tom", "Jerry"],
};
const anotherPerson = createAnother(person);
anotherPerson.sayHi(); // Hi!
console.log(anotherPerson.name); // John
console.log(anotherPerson.friends); // ['Tom', 'Jerry']
// Note: Reference types are still shared
anotherPerson.friends.push("Bob");
console.log(person.friends); // ['Tom', 'Jerry', 'Bob']5. Parasitic Combined Inheritance
This is the most ideal inheritance method, solving the problem of combined inheritance calling the parent constructor twice.
function inheritPrototype(child, parent) {
// Create a copy of the parent class prototype
const prototype = Object.create(parent.prototype);
// Fix constructor reference
prototype.constructor = child;
// Set child class prototype
child.prototype = prototype;
}
function Parent(name, colors) {
this.name = name;
this.colors = colors;
}
Parent.prototype.sayName = function () {
console.log(this.name);
};
function Child(name, age, colors) {
Parent.call(this, name, colors);
this.age = age;
}
// Use parasitic combined inheritance
inheritPrototype(Child, Parent);
// Add child class methods
Child.prototype.sayAge = function () {
console.log(`I am ${this.age} years old`);
};
const child = new Child("John", 25, ["red", "green"]);
child.sayName(); // John
child.sayAge(); // I am 25 years oldAdvantages of Parasitic Combined Inheritance:
- Calls parent constructor only once
- Avoids creating unnecessary properties on child prototype
- Maintains prototype chain unchanged
- Is the most ideal inheritance paradigm
Inheritance Patterns in Practical Applications
1. Implementing Interfaces
JavaScript has no native interface concept, but it can be simulated through prototype inheritance:
// Define interface prototypes
const Drawable = {
draw() {
throw new Error("draw method must be implemented");
},
};
const Movable = {
move(x, y) {
throw new Error("move method must be implemented");
},
};
// Create objects implementing interfaces
function Shape(x, y) {
this.x = x;
this.y = y;
}
// Mix in interfaces
Object.assign(Shape.prototype, Drawable, Movable);
// Concrete classes
function Circle(x, y, radius) {
Shape.call(this, x, y);
this.radius = radius;
}
// Inherit Shape and implement interface methods
Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle;
Circle.prototype.draw = function () {
console.log(
`Drawing circle at (${this.x}, ${this.y}) with radius ${this.radius}`
);
};
Circle.prototype.move = function (x, y) {
this.x += x;
this.y += y;
console.log(`Circle moved to (${this.x}, ${this.y})`);
};
const circle = new Circle(10, 10, 5);
circle.draw(); // Drawing circle at (10, 10) with radius 5
circle.move(5, 5); // Circle moved to (15, 15)2. Implementing Mixin Pattern
Mixin allows combining functionality from multiple objects into one object:
// Define multiple Mixins
const canEat = {
eat() {
console.log(`${this.name} is eating`);
},
};
const canWalk = {
walk() {
console.log(`${this.name} is walking`);
},
};
const canSwim = {
swim() {
console.log(`${this.name} is swimming`);
},
};
// Create base object
function Animal(name) {
this.name = name;
}
// Apply Mixins
function applyMixins(constructor, mixins) {
mixins.forEach((mixin) => {
Object.assign(constructor.prototype, mixin);
});
}
// Different animals combine different abilities
function Dog(name) {
Animal.call(this, name);
}
function Fish(name) {
Animal.call(this, name);
}
function Duck(name) {
Animal.call(this, name);
}
applyMixins(Dog, [canEat, canWalk]);
applyMixins(Fish, [canEat, canSwim]);
applyMixins(Duck, [canEat, canWalk, canSwim]);
const dog = new Dog("Buddy");
dog.eat(); // Buddy is eating
dog.walk(); // Buddy is walking
const fish = new Fish("Nemo");
fish.eat(); // Nemo is eating
fish.swim(); // Nemo is swimming
const duck = new Duck("Donald");
duck.eat(); // Donald is eating
duck.walk(); // Donald is walking
duck.swim(); // Donald is swimming3. Implementing Decorator Pattern
Use prototype inheritance to enhance existing object functionality:
// Base component
function Coffee() {
this.cost = 2;
this.description = "Black coffee";
}
Coffee.prototype.getDescription = function () {
return this.description;
};
Coffee.prototype.getCost = function () {
return this.cost;
};
// Decorator base
function CoffeeDecorator(coffee) {
this.coffee = coffee;
}
CoffeeDecorator.prototype.getDescription = function () {
return this.coffee.getDescription();
};
CoffeeDecorator.prototype.getCost = function () {
return this.coffee.getCost();
};
// Concrete decorators
function MilkDecorator(coffee) {
CoffeeDecorator.call(this, coffee);
this.cost = 0.5;
this.description = "milk";
}
MilkDecorator.prototype = Object.create(CoffeeDecorator.prototype);
MilkDecorator.prototype.constructor = MilkDecorator;
MilkDecorator.prototype.getDescription = function () {
return `${this.coffee.getDescription()}, ${this.description}`;
};
MilkDecorator.prototype.getCost = function () {
return this.coffee.getCost() + this.cost;
};
// Use decorators
const coffee = new Coffee();
const coffeeWithMilk = new MilkDecorator(coffee);
console.log(coffee.getDescription()); // Black coffee
console.log(coffee.getCost()); // 2
console.log(coffeeWithMilk.getDescription()); // Black coffee, milk
console.log(coffeeWithMilk.getCost()); // 2.5Performance Considerations and Best Practices
1. Avoid Deep Prototype Chains
// ❌ Avoid deep prototype chains
function Level1() {}
function Level2() {}
function Level3() {}
function Level4() {}
function Level5() {}
Level2.prototype = Object.create(Level1.prototype);
Level3.prototype = Object.create(Level2.prototype);
Level4.prototype = Object.create(Level3.prototype);
Level5.prototype = Object.create(Level4.prototype);
// ✅ Use composition instead of deep inheritance
class Level5 {
constructor(level1, level2, level3, level4) {
this.level1 = level1;
this.level2 = level2;
this.level3 = level3;
this.level4 = level4;
}
}2. Reasonable Placement of Properties and Methods
function Car(brand, model) {
// Instance-specific properties in constructor
this.brand = brand;
this.model = model;
this.odometer = 0;
}
// Shared methods on prototype
Car.prototype.drive = function (distance) {
this.odometer += distance;
console.log(`${this.brand} ${this.model} drove ${distance} km`);
};
// Private methods can be implemented through closures
const CarFactory = (function () {
function validateBrand(brand) {
return typeof brand === "string" && brand.length > 0;
}
return function (brand, model) {
if (!validateBrand(brand)) {
throw new Error("Invalid brand");
}
this.brand = brand;
this.model = model;
};
})();3. Use Modern Class Syntax
While understanding prototype inheritance is important, it's recommended to use Class syntax in modern projects:
// Modern Class syntax (still prototype-based underneath)
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
speak() {
console.log(`${this.name} barks: Woof!`);
}
fetch() {
console.log(`${this.name} is fetching the ball`);
}
}
const dog = new Dog("Buddy", "Golden Retriever");
dog.speak(); // Buddy barks: Woof!
dog.fetch(); // Buddy is fetching the ballSummary
JavaScript's prototype inheritance provides a flexible mechanism for object inheritance. Understanding different inheritance patterns helps us choose appropriate solutions for different scenarios:
- Prototype chain inheritance: Simple but problematic, suitable for simple scenarios
- Constructor inheritance: Solves shared reference type problem, but cannot reuse methods
- Combined inheritance: Most commonly used, combines advantages of both
- Parasitic combined inheritance: The most ideal inheritance paradigm
- Mixin pattern: Suitable for multiple inheritance scenarios
- Decorator pattern: Suitable for dynamic feature extension
Although modern JavaScript provides Class syntax, understanding the underlying prototype inheritance mechanism is still crucial for mastering JavaScript. In actual development, choose the most suitable inheritance pattern based on specific requirements.