Class Inheritance
Inheritance is a fundamental concept in object-oriented programming that allows you to create new classes based on existing ones. ES6 makes inheritance in JavaScript cleaner and more intuitive with the extends and super keywords.
What is Inheritance?
Inheritance allows a class (child/subclass) to inherit properties and methods from another class (parent/superclass). This promotes code reuse and establishes relationships between classes.
Key Concept: Inheritance creates an "is-a" relationship. For example, a Dog is an Animal, a Car is a Vehicle, and a Manager is an Employee.
The extends Keyword
The extends keyword is used to create a class that inherits from another class:
// Parent class (superclass)
class Animal {
constructor(name, age) {
this.name = name;
this.age = age;
}
makeSound() {
return "Some generic animal sound";
}
info() {
return `${this.name} is ${this.age} years old`;
}
}
// Child class (subclass)
class Dog extends Animal {
constructor(name, age, breed) {
super(name, age); // Call parent constructor
this.breed = breed;
}
makeSound() {
return "Woof! Woof!";
}
fetch() {
return `${this.name} is fetching the ball!`;
}
}
const myDog = new Dog("Max", 3, "Golden Retriever");
console.log(myDog.info()); // "Max is 3 years old" (inherited)
console.log(myDog.makeSound()); // "Woof! Woof!" (overridden)
console.log(myDog.fetch()); // "Max is fetching the ball!" (new method)
console.log(myDog.breed); // "Golden Retriever"
The super Keyword
The super keyword is used to call methods from the parent class:
class Vehicle {
constructor(brand, model) {
this.brand = brand;
this.model = model;
this.speed = 0;
}
accelerate(amount) {
this.speed += amount;
return `Speed: ${this.speed} mph`;
}
describe() {
return `${this.brand} ${this.model}`;
}
}
class Car extends Vehicle {
constructor(brand, model, doors) {
super(brand, model); // Call parent constructor
this.doors = doors;
}
// Override parent method
accelerate(amount) {
// Call parent method
super.accelerate(amount);
return `${this.describe()} accelerating... ${this.speed} mph`;
}
// Add new method
honk() {
return "Beep beep!";
}
}
const myCar = new Car("Toyota", "Camry", 4);
console.log(myCar.accelerate(30)); // "Toyota Camry accelerating... 30 mph"
console.log(myCar.accelerate(20)); // "Toyota Camry accelerating... 50 mph"
console.log(myCar.honk()); // "Beep beep!"
Important: If you define a constructor in a child class, you MUST call super() before accessing this. Otherwise, you'll get a ReferenceError.
Method Overriding
Child classes can override parent methods to provide specialized behavior:
class Shape {
constructor(color) {
this.color = color;
}
area() {
return 0;
}
describe() {
return `A ${this.color} shape with area ${this.area()}`;
}
}
class Rectangle extends Shape {
constructor(color, width, height) {
super(color);
this.width = width;
this.height = height;
}
// Override area method
area() {
return this.width * this.height;
}
}
class Circle extends Shape {
constructor(color, radius) {
super(color);
this.radius = radius;
}
// Override area method
area() {
return Math.PI * this.radius * this.radius;
}
}
const rect = new Rectangle("blue", 10, 5);
const circle = new Circle("red", 7);
console.log(rect.describe()); // "A blue shape with area 50"
console.log(circle.describe()); // "A red shape with area 153.938..."
Inheritance Chains
Classes can form inheritance chains where a child class can itself be a parent to another class:
class LivingBeing {
constructor(name) {
this.name = name;
this.isAlive = true;
}
breathe() {
return `${this.name} is breathing`;
}
}
class Animal extends LivingBeing {
constructor(name, species) {
super(name);
this.species = species;
}
move() {
return `${this.name} is moving`;
}
}
class Mammal extends Animal {
constructor(name, species, furColor) {
super(name, species);
this.furColor = furColor;
}
nurse() {
return `${this.name} is nursing its young`;
}
}
class Dog extends Mammal {
constructor(name, breed, furColor) {
super(name, "Canine", furColor);
this.breed = breed;
}
bark() {
return `${this.name} says: Woof!`;
}
}
const myDog = new Dog("Buddy", "Labrador", "Golden");
console.log(myDog.breathe()); // From LivingBeing
console.log(myDog.move()); // From Animal
console.log(myDog.nurse()); // From Mammal
console.log(myDog.bark()); // From Dog
console.log(myDog.species); // "Canine"
Caution: Deep inheritance chains can make code harder to understand and maintain. Generally, avoid going more than 2-3 levels deep. Consider composition as an alternative.
The instanceof Operator
Use instanceof to check if an object is an instance of a class (or its parent classes):
class Animal {
constructor(name) {
this.name = name;
}
}
class Dog extends Animal {
bark() {
return "Woof!";
}
}
class Cat extends Animal {
meow() {
return "Meow!";
}
}
const dog = new Dog("Max");
const cat = new Cat("Whiskers");
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
console.log(dog instanceof Cat); // false
console.log(cat instanceof Cat); // true
console.log(cat instanceof Animal); // true
console.log(cat instanceof Dog); // false
// Useful for type checking
function makeAnimalSound(animal) {
if (animal instanceof Dog) {
return animal.bark();
} else if (animal instanceof Cat) {
return animal.meow();
}
return "Unknown animal";
}
console.log(makeAnimalSound(dog)); // "Woof!"
console.log(makeAnimalSound(cat)); // "Meow!"
Protecting Methods with super
You can use super to extend parent functionality rather than completely replacing it:
class BankAccount {
constructor(accountNumber, balance) {
this.accountNumber = accountNumber;
this.balance = balance;
this.transactions = [];
}
deposit(amount) {
this.balance += amount;
this.transactions.push({ type: "deposit", amount, date: new Date() });
return `Deposited $${amount}. New balance: $${this.balance}`;
}
withdraw(amount) {
if (amount > this.balance) {
return "Insufficient funds";
}
this.balance -= amount;
this.transactions.push({ type: "withdrawal", amount, date: new Date() });
return `Withdrew $${amount}. New balance: $${this.balance}`;
}
}
class SavingsAccount extends BankAccount {
constructor(accountNumber, balance, interestRate) {
super(accountNumber, balance);
this.interestRate = interestRate;
this.withdrawalLimit = 6; // Federal regulation
this.withdrawalCount = 0;
}
withdraw(amount) {
if (this.withdrawalCount >= this.withdrawalLimit) {
return "Monthly withdrawal limit reached";
}
// Call parent withdraw method
const result = super.withdraw(amount);
if (result.includes("Withdrew")) {
this.withdrawalCount++;
}
return result;
}
addInterest() {
const interest = this.balance * (this.interestRate / 100);
this.deposit(interest);
return `Interest added: $${interest.toFixed(2)}`;
}
resetWithdrawals() {
this.withdrawalCount = 0;
return "Monthly withdrawal count reset";
}
}
const savings = new SavingsAccount("SAV-001", 1000, 2.5);
console.log(savings.deposit(500)); // "Deposited $500. New balance: $1500"
console.log(savings.withdraw(100)); // "Withdrew $100. New balance: $1400"
console.log(savings.addInterest()); // "Interest added: $35.00"
Composition vs Inheritance
While inheritance is powerful, composition (combining objects) is often a better choice:
// Inheritance approach (can get messy)
class FlyingAnimal extends Animal {
fly() { return "Flying"; }
}
class SwimmingAnimal extends Animal {
swim() { return "Swimming"; }
}
// What about animals that both fly and swim?
// Composition approach (more flexible)
class Animal {
constructor(name, abilities = []) {
this.name = name;
this.abilities = abilities;
}
can(ability) {
return this.abilities.includes(ability);
}
perform(action) {
if (this.can(action)) {
return `${this.name} is ${action}`;
}
return `${this.name} cannot ${action}`;
}
}
// Create ability objects
const flying = {
fly() {
return "flying high";
}
};
const swimming = {
swim() {
return "swimming gracefully";
}
};
const walking = {
walk() {
return "walking on land";
}
};
// Compose abilities
const duck = new Animal("Duck", ["flying", "swimming", "walking"]);
const fish = new Animal("Fish", ["swimming"]);
const bird = new Animal("Eagle", ["flying", "walking"]);
console.log(duck.perform("flying")); // "Duck is flying"
console.log(duck.perform("swimming")); // "Duck is swimming"
console.log(fish.perform("flying")); // "Fish cannot flying"
Best Practice: Prefer composition over inheritance when possible. Use inheritance for true "is-a" relationships and composition for "has-a" or "can-do" relationships.
Static Methods and Inheritance
Static methods are also inherited by child classes:
class MathOperations {
static add(a, b) {
return a + b;
}
static multiply(a, b) {
return a * b;
}
}
class AdvancedMath extends MathOperations {
static power(base, exponent) {
return Math.pow(base, exponent);
}
static squareRoot(num) {
return Math.sqrt(num);
}
}
// Child class has access to parent static methods
console.log(AdvancedMath.add(5, 3)); // 8 (inherited)
console.log(AdvancedMath.multiply(4, 6)); // 24 (inherited)
console.log(AdvancedMath.power(2, 3)); // 8 (own method)
console.log(AdvancedMath.squareRoot(16)); // 4 (own method)
Practice Exercise:
Challenge: Create an employee management system with the following requirements:
Employee class: name, id, salary, and calculateBonus() method (10% of salary)
Manager extends Employee: add teamSize, override calculateBonus() (15% + $100 per team member)
Developer extends Employee: add programmingLanguages array, add method to add languages
Designer extends Employee: add designTools array, override calculateBonus() (12% of salary)
Solution:
class Employee {
constructor(name, id, salary) {
this.name = name;
this.id = id;
this.salary = salary;
}
calculateBonus() {
return this.salary * 0.10;
}
getInfo() {
return `${this.name} (ID: ${this.id}) - Salary: $${this.salary}`;
}
}
class Manager extends Employee {
constructor(name, id, salary, teamSize) {
super(name, id, salary);
this.teamSize = teamSize;
}
calculateBonus() {
return (this.salary * 0.15) + (this.teamSize * 100);
}
}
class Developer extends Employee {
constructor(name, id, salary, programmingLanguages = []) {
super(name, id, salary);
this.programmingLanguages = programmingLanguages;
}
addLanguage(language) {
if (!this.programmingLanguages.includes(language)) {
this.programmingLanguages.push(language);
}
return `${language} added. Known languages: ${this.programmingLanguages.join(", ")}`;
}
}
class Designer extends Employee {
constructor(name, id, salary, designTools = []) {
super(name, id, salary);
this.designTools = designTools;
}
calculateBonus() {
return this.salary * 0.12;
}
}
// Test the system
const manager = new Manager("Alice", "M001", 80000, 5);
console.log(manager.getInfo()); // "Alice (ID: M001) - Salary: $80000"
console.log("Bonus: $" + manager.calculateBonus()); // Bonus: $12500
const dev = new Developer("Bob", "D001", 70000, ["JavaScript"]);
console.log(dev.addLanguage("Python")); // "Python added. Known languages: JavaScript, Python"
console.log("Bonus: $" + dev.calculateBonus()); // Bonus: $7000
const designer = new Designer("Carol", "DS001", 65000, ["Figma", "Photoshop"]);
console.log("Bonus: $" + designer.calculateBonus()); // Bonus: $7800
Summary
In this lesson, you learned:
- The
extends keyword creates inheritance relationships
- The
super keyword calls parent class methods and constructors
- Child classes can override parent methods for specialized behavior
- Inheritance chains allow multi-level class hierarchies
- The
instanceof operator checks class membership
- Composition is often preferable to deep inheritance
- Static methods are inherited by child classes
Next Up: In the next lesson, we'll dive deep into prototypes and the prototype chain to understand what's happening behind the scenes with classes and inheritance!