Skip to main content

JavaScript Inheritance

Inheritance is a fundamental concept in object-oriented programming that allows you to create new classes based on existing ones. In JavaScript, inheritance works differently than in traditional class-based languages, using a prototype-based approach. With the introduction of ES6 classes, JavaScript now offers a more familiar syntax for inheritance while still using prototypes under the hood.

Introduction to JavaScript Inheritance

Inheritance enables code reuse by allowing objects to inherit properties and methods from other objects. This creates a parent-child relationship between objects, where the child (derived) object can access and extend the functionality of the parent (base) object.

In JavaScript, inheritance can be implemented through:

  • Prototype-based inheritance (the traditional JavaScript way)
  • Class-based inheritance (introduced in ES6)

Let's explore both approaches to understand how inheritance works in JavaScript.

Prototype-based Inheritance

JavaScript is primarily a prototype-based language. Every object in JavaScript has a hidden property called [[Prototype]] (accessed via __proto__ or Object.getPrototypeOf()) that links to another object called its prototype.

The Prototype Chain

When you try to access a property or method on an object, JavaScript first looks for it directly on the object. If it doesn't find it, it looks up the prototype chain:

javascript
// Base object
const animal = {
isAlive: true,
eat: function() {
return "Eating...";
},
sleep: function() {
return "Sleeping...";
}
};

// Create an object that inherits from animal
const dog = Object.create(animal);
dog.bark = function() {
return "Woof!";
};

console.log(dog.isAlive); // true (inherited from animal)
console.log(dog.eat()); // "Eating..." (inherited from animal)
console.log(dog.bark()); // "Woof!" (defined on dog)

In this example, dog inherits properties and methods from animal through the prototype chain.

Constructor Functions and Inheritance

Before ES6 classes, constructor functions were commonly used to implement inheritance:

javascript
// Parent constructor
function Animal(name) {
this.name = name;
this.isAlive = true;
}

// Add methods to Animal prototype
Animal.prototype.eat = function() {
return `${this.name} is eating.`;
};

Animal.prototype.sleep = function() {
return `${this.name} is sleeping.`;
};

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

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

// Add methods specific to Dog
Dog.prototype.bark = function() {
return `${this.name} says woof!`;
};

// Create instances
const myAnimal = new Animal("Generic Animal");
const myDog = new Dog("Rex", "German Shepherd");

console.log(myDog.name); // "Rex"
console.log(myDog.breed); // "German Shepherd"
console.log(myDog.eat()); // "Rex is eating."
console.log(myDog.bark()); // "Rex says woof!"
console.log(myDog instanceof Dog); // true
console.log(myDog instanceof Animal); // true

This example shows the traditional way to implement inheritance in JavaScript. It's verbose and requires several steps to set up properly.

Class-based Inheritance (ES6)

ES6 introduced a cleaner syntax for implementing inheritance using the class and extends keywords. Under the hood, it still uses prototypes, but the syntax is more familiar to developers from other OOP languages.

javascript
// Parent class
class Animal {
constructor(name) {
this.name = name;
this.isAlive = true;
}

eat() {
return `${this.name} is eating.`;
}

sleep() {
return `${this.name} is sleeping.`;
}
}

// Child class extending parent
class Dog extends Animal {
constructor(name, breed) {
super(name); // Call parent constructor
this.breed = breed;
}

bark() {
return `${this.name} says woof!`;
}

// Override parent method
sleep() {
return `${this.name} is sleeping in a dog bed.`;
}
}

// Create instances
const myAnimal = new Animal("Generic Animal");
const myDog = new Dog("Rex", "German Shepherd");

console.log(myDog.name); // "Rex"
console.log(myDog.breed); // "German Shepherd"
console.log(myDog.eat()); // "Rex is eating."
console.log(myDog.sleep()); // "Rex is sleeping in a dog bed."
console.log(myDog.bark()); // "Rex says woof!"
console.log(myDog instanceof Dog); // true
console.log(myDog instanceof Animal); // true

In this example:

  1. Dog class extends the Animal class using the extends keyword
  2. super() is used in the constructor to call the parent constructor
  3. The sleep() method is overridden in the Dog class
  4. The bark() method is added to the Dog class

Key Points About super

The super keyword is used to:

  1. Call the parent constructor with super()
  2. Call a parent's method with super.methodName()
javascript
class Dog extends Animal {
constructor(name, breed) {
super(name); // Call parent constructor
this.breed = breed;
}

sleep() {
// Call parent's sleep method first
const parentMessage = super.sleep();
return `${parentMessage} And dreaming of bones.`;
}
}

const myDog = new Dog("Rex", "German Shepherd");
console.log(myDog.sleep()); // "Rex is sleeping. And dreaming of bones."

Multi-level Inheritance

JavaScript supports multi-level inheritance, where a class can extend another class that extends yet another class.

javascript
// Grandparent class
class Animal {
constructor(name) {
this.name = name;
this.isAlive = true;
}

eat() {
return `${this.name} is eating.`;
}
}

// Parent class
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}

bark() {
return `${this.name} says woof!`;
}
}

// Child class
class WorkingDog extends Dog {
constructor(name, breed, job) {
super(name, breed);
this.job = job;
}

work() {
return `${this.name} is working as a ${this.job}.`;
}
}

const myWorkingDog = new WorkingDog("Max", "Border Collie", "herder");
console.log(myWorkingDog.name); // "Max"
console.log(myWorkingDog.breed); // "Border Collie"
console.log(myWorkingDog.job); // "herder"
console.log(myWorkingDog.eat()); // "Max is eating."
console.log(myWorkingDog.bark()); // "Max says woof!"
console.log(myWorkingDog.work()); // "Max is working as a herder."

Practical Example: Building a UI Component System

Let's see a more practical example of inheritance by building a simple UI component system:

javascript
// Base Component class
class Component {
constructor(id, theme = 'light') {
this.id = id;
this.theme = theme;
this.element = null;
}

render() {
throw new Error('You must implement the render method');
}

mount(parent) {
if (!this.element) {
this.element = this.render();
}
document.getElementById(parent).appendChild(this.element);
return this;
}

setTheme(theme) {
this.theme = theme;
if (this.element) {
this.element.dataset.theme = theme;
}
return this;
}
}

// Button Component
class Button extends Component {
constructor(id, text, onClick, theme) {
super(id, theme); // Call parent constructor
this.text = text;
this.onClick = onClick;
}

render() {
const button = document.createElement('button');
button.id = this.id;
button.innerText = this.text;
button.dataset.theme = this.theme;
button.classList.add('ui-button');
button.addEventListener('click', this.onClick);
return button;
}
}

// Modal Component
class Modal extends Component {
constructor(id, title, content, theme) {
super(id, theme);
this.title = title;
this.content = content;
this.isOpen = false;
}

render() {
const modal = document.createElement('div');
modal.id = this.id;
modal.dataset.theme = this.theme;
modal.classList.add('ui-modal');
modal.style.display = 'none';

const header = document.createElement('div');
header.classList.add('modal-header');

const titleElement = document.createElement('h2');
titleElement.innerText = this.title;

const closeButton = document.createElement('span');
closeButton.innerText = '×';
closeButton.classList.add('close-button');
closeButton.addEventListener('click', () => this.close());

const body = document.createElement('div');
body.classList.add('modal-body');
body.innerHTML = this.content;

header.appendChild(titleElement);
header.appendChild(closeButton);
modal.appendChild(header);
modal.appendChild(body);

return modal;
}

open() {
if (this.element) {
this.element.style.display = 'block';
this.isOpen = true;
}
return this;
}

close() {
if (this.element) {
this.element.style.display = 'none';
this.isOpen = false;
}
return this;
}
}

// Usage example
// Assume there's a div with id="app" in your HTML
document.addEventListener('DOMContentLoaded', () => {
const openModalBtn = new Button(
'openModalBtn',
'Open Modal',
() => myModal.open(),
'dark'
).mount('app');

const myModal = new Modal(
'myFirstModal',
'Welcome!',
'<p>This is a modal created with our component system.</p>',
'light'
).mount('app');
});

This example demonstrates how inheritance helps create a reusable component system. Both Button and Modal inherit common functionality from the Component class while implementing their specific behaviors.

When to Use Inheritance

While inheritance is powerful, it's not always the best solution. Consider these guidelines:

  1. Use inheritance when:

    • You have "is-a" relationships (e.g., a dog is an animal)
    • You want to reuse code across similar objects
    • You're building a class hierarchy with clear parent-child relationships
  2. Avoid inheritance when:

    • You just need to share some utility methods (use composition instead)
    • The relationship between objects is complex or likely to change
    • You find yourself creating deep inheritance hierarchies

Remember the principle: "Favor composition over inheritance" when appropriate.

Composition vs. Inheritance

An alternative to inheritance is composition, where you build complex objects by combining simpler ones:

javascript
// Using composition instead of inheritance
class Animal {
constructor(name) {
this.name = name;
this.behaviors = [];
}

addBehavior(behavior) {
this.behaviors.push(behavior);
return this;
}

performBehaviors() {
return this.behaviors.map(behavior =>
behavior.perform(this.name)
).join('\n');
}
}

// Behaviors as separate objects
const eatingBehavior = {
perform: (name) => `${name} is eating.`
};

const sleepingBehavior = {
perform: (name) => `${name} is sleeping.`
};

const barkingBehavior = {
perform: (name) => `${name} says woof!`
};

// Create animals with different behaviors
const genericAnimal = new Animal("Generic Animal")
.addBehavior(eatingBehavior)
.addBehavior(sleepingBehavior);

const dog = new Animal("Rex")
.addBehavior(eatingBehavior)
.addBehavior(sleepingBehavior)
.addBehavior(barkingBehavior);

console.log(genericAnimal.performBehaviors());
// "Generic Animal is eating.
// Generic Animal is sleeping."

console.log(dog.performBehaviors());
// "Rex is eating.
// Rex is sleeping.
// Rex says woof!"

This approach gives more flexibility than inheritance and often leads to more maintainable code.

Summary

JavaScript inheritance is a powerful feature that enables code reuse and the creation of object hierarchies. In this tutorial, we've covered:

  • JavaScript's prototype-based inheritance system
  • Implementing inheritance with constructor functions
  • Using ES6 classes and the extends keyword for cleaner inheritance
  • Multi-level inheritance for deeper object hierarchies
  • A practical example of inheritance in a UI component system
  • When to use inheritance vs. composition

Understanding inheritance is crucial for effective object-oriented programming in JavaScript. However, remember to use it judiciously and consider composition when it provides a better solution for your specific needs.

Exercises

  1. Create a Vehicle class with properties make, model, and year, and a method getDetails(). Then create Car and Motorcycle classes that inherit from Vehicle and add their own specific properties and methods.

  2. Extend the UI component system example by creating a Form component that inherits from Component and includes validation logic.

  3. Refactor the inheritance-based example of Animal and Dog into a composition-based design.

  4. Create a class hierarchy for a simple game with a Character base class and derived classes like Warrior, Mage, and Archer.

Additional Resources



If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)