Skip to content

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

javascript
// 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); // true

Prototype 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.

javascript
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.

javascript
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); // false

Advantages:

  • 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.

javascript
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); // true

Improved Version of Combined Inheritance:

javascript
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.

javascript
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.

javascript
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 old

Advantages 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:

javascript
// 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:

javascript
// 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 swimming

3. Implementing Decorator Pattern

Use prototype inheritance to enhance existing object functionality:

javascript
// 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.5

Performance Considerations and Best Practices

1. Avoid Deep Prototype Chains

javascript
// ❌ 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

javascript
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:

javascript
// 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 ball

Summary

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.