Programming

JavaScript Inheritance: Ultimate Guide to OOP in Javascript

We are going to dive deep into one of the most powerful concepts in JavaScript – inheritance. Whether you’re just starting out or looking to sharpen your skills, this guide will absolutely transform how you approach object-oriented programming in JavaScript.

Why JavaScript Inheritance Matters (Still)

Even with all the modern JavaScript frameworks and libraries we have today, understanding inheritance remains crucial for writing clean, maintainable code. Trust me, this concept isn’t going anywhere – it’s the foundation of how objects relate to each other in your applications.

JavaScript has evolved dramatically since I first wrote about this topic, but inheritance continues to be a cornerstone concept that separates novice developers from the pros. The language now offers multiple ways to implement inheritance, and knowing which approach to use can dramatically improve your code quality

JavaScript Inheritance: The Complete Picture

JavaScript inheritance has evolved significantly over the years. Let’s explore both the traditional prototype-based approach and the modern class-based syntax.

Prototype-Based Inheritance

The prototype chain is the original JavaScript inheritance mechanism. Every JavaScript object has a prototype from which it inherits properties and methods.

// Parent "class"
function Animal(name) {
  this.name = name;
}

Animal.prototype.makeSound = function() {
  return "Some generic sound";
};

// Child "class"
function Dog(name, breed) {
  // Call the parent constructor
  Animal.call(this, name);
  this.breed = breed;
}

// Set up inheritance
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // Fix the constructor pointer

// Override parent method
Dog.prototype.makeSound = function() {
  return "Woof!";
};

// Add a method specific to Dog
Dog.prototype.fetch = function() {
  return `${this.name} is fetching the ball!`;
};

const rex = new Dog("Rex", "German Shepherd");
console.log(rex.name); // "Rex"
console.log(rex.makeSound()); // "Woof!"
console.log(rex.fetch()); // "Rex is fetching the ball!"Code language: JavaScript (javascript)

This approach works perfectly, but the syntax can be a bit verbose and confusing. This is why ES6 introduced class syntax.

Modern Class-Based Inheritance (ES6+)

ES6 classes provide a cleaner, more familiar syntax for creating objects and implementing inheritance. Under the hood, they still use prototypes, but the syntax is much more intuitive:

class Animal {
  constructor(name) {
    this.name = name;
  }
  
  makeSound() {
    return "Some generic sound";
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // Call parent constructor
    this.breed = breed;
  }
  
  // Override parent method
  makeSound() {
    return "Woof!";
  }
  
  // Add a method specific to Dog
  fetch() {
    return `${this.name} is fetching the ball!`;
  }
}

const rex = new Dog("Rex", "German Shepherd");
console.log(rex.name); // "Rex"
console.log(rex.makeSound()); // "Woof!"
console.log(rex.fetch()); // "Rex is fetching the ball!"Code language: JavaScript (javascript)

Isn’t that much cleaner? The extends keyword sets up the prototype chain for you, and the super keyword calls the parent class constructor.

Advanced Inheritance Techniques

The Prototype Chain in Depth

Understanding how the prototype chain works is essential for mastering JavaScript inheritance. When you access a property or method on an object, JavaScript looks for it in the object itself. If it doesn’t find it, it looks in the object’s prototype, then that prototype’s prototype, and so on up the chain until it reaches Object.prototype.

const animal = new Animal("Generic Animal");
console.log(animal.__proto__ === Animal.prototype); // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true

const dog = new Dog("Buddy", "Golden Retriever");
console.log(dog.__proto__ === Dog.prototype); // true
console.log(Dog.prototype.__proto__ === Animal.prototype); // trueCode language: JavaScript (javascript)

Multiple Inheritance and Mixins

JavaScript doesn’t support multiple inheritance directly, but you can achieve similar functionality using mixins:

// Mixin - a collection of methods
const swimMixin = {
  swim() {
    return `${this.name} is swimming`;
  }
};

const flyMixin = {
  fly() {
    return `${this.name} is flying`;
  }
};

// Apply mixins to a class
Object.assign(Dog.prototype, swimMixin);

const buddy = new Dog("Buddy", "Labrador");
console.log(buddy.makeSound()); // "Woof!"
console.log(buddy.swim()); // "Buddy is swimming"Code language: JavaScript (javascript)

Practical Inheritance Patterns

The Factory Pattern

The factory pattern creates objects without exposing the instantiation logic:

function createAnimal(type, name) {
  const animal = { name };
  
  if (type === "dog") {
    return Object.assign(animal, {
      makeSound() { return "Woof!"; },
      fetch() { return `${name} is fetching`; }
    });
  } else if (type === "cat") {
    return Object.assign(animal, {
      makeSound() { return "Meow!"; },
      climb() { return `${name} is climbing`; }
    });
  }
  
  return animal;
}

const rex = createAnimal("dog", "Rex");
console.log(rex.makeSound()); // "Woof!"Code language: JavaScript (javascript)

The Constructor Pattern

The constructor pattern uses constructor functions to create specific types of objects:

function Animal(name) {
  this.name = name;
}

function Dog(name, breed) {
  Animal.call(this, name);
  this.breed = breed;
}

// Set up inheritance
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;Code language: JavaScript (javascript)

The Module Pattern

The module pattern allows for privacy through closures:

const AnimalModule = (function() {
  // Private variables
  const privateData = {};
  
  // Public API
  return {
    createAnimal(name) {
      const id = Date.now();
      privateData[id] = { health: 100 };
      
      return {
        name,
        getHealth() {
          return privateData[id].health;
        },
        decreaseHealth(amount) {
          privateData[id].health -= amount;
        }
      };
    }
  };
})();

const animal = AnimalModule.createAnimal("Mystery Creature");
console.log(animal.getHealth()); // 100
animal.decreaseHealth(20);
console.log(animal.getHealth()); // 80Code language: JavaScript (javascript)

Best Practices for JavaScript Inheritance

  1. Favor composition over inheritance – Instead of deep inheritance hierarchies, compose objects from smaller, reusable parts.
  2. Keep the prototype chain shallow – Deep prototype chains can lead to performance issues.
  3. Use ES6 classes for cleaner syntax – They make your code more readable and easier to maintain.
  4. Be consistent with your approach – Don’t mix different inheritance patterns in the same codebase.
  5. Document your inheritance structure – Make sure other developers can understand the relationships between your objects.

Common Inheritance Pitfalls and How to Avoid Them

Forgetting to Call the Parent Constructor

When extending a class, always call the parent constructor using super() in ES6 classes or ParentClass.call(this, ...) in the traditional approach.

Modifying the Built-in Prototypes

Modifying prototypes of built-in objects like Array or Object can lead to unpredictable behavior and conflicts with libraries. Avoid this practice!

// Don't do this!
Array.prototype.first = function() {
  return this[0];
};Code language: JavaScript (javascript)

Confusion Between __proto__ and prototype

Remember that prototype is a property of constructor functions, while __proto__ (or Object.getPrototypeOf()) accesses the prototype of an instance.

Conclusion: Mastering JavaScript Inheritance

JavaScript inheritance is a powerful tool that helps you write cleaner, more maintainable code. Whether you prefer the traditional prototype-based approach or the modern class syntax, understanding these concepts will make you a better JavaScript developer.

The patterns and techniques we’ve explored are used in countless JavaScript libraries and frameworks, so mastering them will help you read and contribute to a wider range of codebases. MDN Documentation is also very good read to learn more.

What’s your favorite inheritance pattern? Have any questions about implementing these techniques in your own projects? Drop a comment below, and I’d be happy to help!

Rana Ahsan

Rana Ahsan is a seasoned software engineer and technology leader specialized in distributed systems and software architecture. With a Master’s in Software Engineering from Concordia University, his experience spans leading scalable architecture at Coursera and TopHat, contributing to open-source projects. This blog, CodeSamplez.com, showcases his passion for sharing practical insights on programming and distributed systems concepts and help educate others. Github | X | LinkedIn

View Comments

Recent Posts

Service Workers in React: Framework Integration Guide

Learn how to integrate service workers in React, Next.js, Vue, and Angular with practical code examples and production-ready implementations for modern web applications.

2 weeks ago

Service Worker Caching Strategies: Performance & Offline Apps

Master the essential service worker caching strategies that transform web performance. Learn Cache-First, Network-First, and Stale-While-Revalidate patterns with practical examples that'll make your apps blazingly…

3 weeks ago

Service Worker Lifecycle: Complete Guide for FE Developers

Master the intricate dance of service worker states and events that power modern PWAs. From registration through installation, activation, and termination, understanding the lifecycle unlocks…

4 weeks ago

This website uses cookies.