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:
// 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:
// 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.
// 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:
Dog
class extends theAnimal
class using theextends
keywordsuper()
is used in the constructor to call the parent constructor- The
sleep()
method is overridden in theDog
class - The
bark()
method is added to theDog
class
Key Points About super
The super
keyword is used to:
- Call the parent constructor with
super()
- Call a parent's method with
super.methodName()
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.
// 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:
// 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:
-
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
-
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:
// 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
-
Create a
Vehicle
class with propertiesmake
,model
, andyear
, and a methodgetDetails()
. Then createCar
andMotorcycle
classes that inherit fromVehicle
and add their own specific properties and methods. -
Extend the UI component system example by creating a
Form
component that inherits fromComponent
and includes validation logic. -
Refactor the inheritance-based example of
Animal
andDog
into a composition-based design. -
Create a class hierarchy for a simple game with a
Character
base class and derived classes likeWarrior
,Mage
, andArcher
.
Additional Resources
If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)