Skip to main content
How do game developers create hundreds of character types without copy-pasting the same code over and over? How can a Warrior, Mage, and Archer all “attack” differently but be treated the same way in battle?
// One base class, infinite possibilities
class Character {
  constructor(name) {
    this.name = name
    this.health = 100
  }
  
  attack() {
    return `${this.name} attacks!`
  }
}

class Warrior extends Character {
  attack() {
    return `${this.name} swings a mighty sword!`
  }
}

class Mage extends Character {
  attack() {
    return `${this.name} casts a fireball!`
  }
}

const hero = new Warrior("Aragorn")
const wizard = new Mage("Gandalf")

console.log(hero.attack())    // "Aragorn swings a mighty sword!"
console.log(wizard.attack())  // "Gandalf casts a fireball!"
The answer lies in two powerful OOP principles: inheritance lets classes share code by extending other classes, and polymorphism lets different objects respond to the same method call in their own unique way. These concepts, formalized in the ECMAScript 2015 specification through the class and extends keywords, brought familiar OOP patterns to JavaScript’s prototype-based model.
What you’ll learn in this guide:
  • How inheritance lets child classes reuse parent class code
  • Using the extends keyword to create class hierarchies
  • The super keyword for calling parent constructors and methods
  • Method overriding for specialized behavior
  • Polymorphism: treating different object types through a common interface
  • When to use composition instead of inheritance (the Gorilla-Banana problem)
  • Mixins for sharing behavior across unrelated classes
Prerequisites: This guide assumes you understand Factories & Classes and Object Creation & Prototypes. If you’re not comfortable with creating classes in JavaScript, read those guides first!

What is Inheritance?

Inheritance is a mechanism where a class (called a child or subclass) can inherit properties and methods from another class (called a parent or superclass). Instead of rewriting common functionality, the child class automatically gets everything the parent has — and can add or customize as needed. Think of it as the “IS-A” relationship:
  • A Warrior IS-A Character — so it inherits all Character traits
  • A Mage IS-A Character — same base, different specialization
  • An Archer IS-A Character — you get the pattern
// The parent class — all characters share these basics
class Character {
  constructor(name, health = 100) {
    this.name = name
    this.health = health
  }
  
  introduce() {
    return `I am ${this.name} with ${this.health} HP`
  }
  
  attack() {
    return `${this.name} attacks!`
  }
  
  takeDamage(amount) {
    this.health -= amount
    return `${this.name} takes ${amount} damage! (${this.health} HP left)`
  }
}

// The child class — gets everything from Character automatically
class Warrior extends Character {
  constructor(name) {
    super(name, 150)  // Warriors have more health!
    this.rage = 0
  }
  
  // New method only Warriors have
  battleCry() {
    this.rage += 10
    return `${this.name} roars with fury! Rage: ${this.rage}`
  }
}

const conan = new Warrior("Conan")
console.log(conan.introduce())   // "I am Conan with 150 HP" (inherited!)
console.log(conan.battleCry())   // "Conan roars with fury! Rage: 10" (new!)
console.log(conan.attack())      // "Conan attacks!" (inherited!)
The DRY Principle: Inheritance helps you “Don’t Repeat Yourself”. Write common code once in the parent class, and all children automatically benefit — including bug fixes and improvements! As the Gang of Four noted in Design Patterns, favoring composition over deep inheritance hierarchies often leads to more maintainable code.

The Game Character Analogy

Imagine you’re building an RPG game. Every character — whether player or enemy — shares basic traits: a name, health points, the ability to attack and take damage. But each character type has unique abilities.
┌─────────────────────────────────────────────────────────────────────────┐
│                        GAME CHARACTER HIERARCHY                          │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│                          ┌───────────────┐                               │
│                          │   Character   │  ← Parent (base class)        │
│                          │   ─────────   │                               │
│                          │  name         │                               │
│                          │  health       │                               │
│                          │  attack()     │                               │
│                          │  takeDamage() │                               │
│                          └───────┬───────┘                               │
│                                  │                                       │
│             ┌────────────────────┼────────────────────┐                  │
│             │                    │                    │                  │
│             ▼                    ▼                    ▼                  │
│      ┌─────────────┐      ┌─────────────┐      ┌─────────────┐          │
│      │   Warrior   │      │    Mage     │      │   Archer    │          │
│      │   ───────   │      │   ──────    │      │   ──────    │          │
│      │  rage       │      │  mana       │      │  arrows     │          │
│      │  battleCry()│      │  castSpell()│      │  aim()      │          │
│      │  attack() ⚔ │      │  attack() ✨│      │  attack() 🏹│          │
│      └─────────────┘      └─────────────┘      └─────────────┘          │
│                                                                          │
│      Each child INHERITS from Character but OVERRIDES attack()          │
│      to provide specialized behavior — that's POLYMORPHISM!              │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘
Without inheritance, you’d copy-paste name, health, takeDamage() into every character class. With inheritance, you write it once and extend it:
class Warrior extends Character { /* ... */ }
class Mage extends Character { /* ... */ }
class Archer extends Character { /* ... */ }
Each child class automatically has everything Character has, plus their own unique additions.

Class Inheritance with extends

The extends keyword creates a class that is a child of another class. The syntax is straightforward:
class ChildClass extends ParentClass {
  // Child-specific code here
}
1

Define the Parent Class

Create the base class with shared properties and methods:
class Character {
  constructor(name) {
    this.name = name
    this.health = 100
  }
  
  attack() {
    return `${this.name} attacks!`
  }
}
2

Create a Child Class with extends

Use extends to inherit from the parent:
class Mage extends Character {
  constructor(name) {
    super(name)        // Call parent constructor FIRST
    this.mana = 100    // Then add child-specific properties
  }
  
  castSpell(spell) {
    this.mana -= 10
    return `${this.name} casts ${spell}!`
  }
}
3

Use the Child Class

Instances have both parent AND child capabilities:
const gandalf = new Mage("Gandalf")

// Inherited from Character
console.log(gandalf.name)       // "Gandalf"
console.log(gandalf.health)     // 100
console.log(gandalf.attack())   // "Gandalf attacks!"

// Unique to Mage
console.log(gandalf.mana)           // 100
console.log(gandalf.castSpell("Fireball"))  // "Gandalf casts Fireball!"

What the Child Automatically Gets

When you use extends, the child class inherits:
InheritedExample
Instance propertiesthis.name, this.health
Instance methodsattack(), takeDamage()
Static methodsCharacter.createRandom() (if defined)
Getters/Settersget isAlive(), set health(val)
class Character {
  constructor(name) {
    this.name = name
    this.health = 100
  }
  
  get isAlive() {
    return this.health > 0
  }
  
  static createRandom() {
    const names = ["Hero", "Villain", "Sidekick"]
    return new this(names[Math.floor(Math.random() * names.length)])
  }
}

class Warrior extends Character {
  constructor(name) {
    super(name)
    this.rage = 0
  }
}

// Child inherits the static method!
const randomWarrior = Warrior.createRandom()
console.log(randomWarrior.name)     // Random name
console.log(randomWarrior.isAlive)  // true (inherited getter)
console.log(randomWarrior.rage)     // 0 (Warrior-specific)

The super Keyword

The super keyword is your lifeline when working with inheritance. It has two main uses:

1. super() — Calling the Parent Constructor

When a child class has a constructor, it must call super() before using this. This runs the parent’s constructor to set up inherited properties.
class Character {
  constructor(name, health) {
    this.name = name
    this.health = health
  }
}

class Warrior extends Character {
  constructor(name) {
    // MUST call super() first!
    super(name, 150)     // Pass arguments to parent constructor
    
    // Now we can use 'this'
    this.rage = 0
    this.weapon = "Sword"
  }
}

const warrior = new Warrior("Conan")
console.log(warrior.name)    // "Conan" (set by parent)
console.log(warrior.health)  // 150 (passed to parent)
console.log(warrior.rage)    // 0 (set by child)
Critical Rule: You MUST call super() before accessing this in a child constructor. If you don’t, JavaScript throws a ReferenceError:
class Warrior extends Character {
  constructor(name) {
    this.rage = 0  // ❌ ReferenceError: Must call super constructor first!
    super(name)
  }
}

2. super.method() — Calling Parent Methods

Use super.methodName() to call the parent’s version of an overridden method. This is perfect when you want to extend behavior rather than replace it:
class Character {
  constructor(name, health = 100) {
    this.name = name
    this.health = health
  }
  
  attack() {
    return `${this.name} attacks`
  }
  
  describe() {
    return `${this.name} (${this.health} HP)`
  }
}

class Warrior extends Character {
  constructor(name) {
    super(name, 150)  // Pass name and custom health to parent
    this.weapon = "Sword"
  }
  
  attack() {
    // Call parent's attack, then add to it
    const baseAttack = super.attack()
    return `${baseAttack} with a ${this.weapon}!`
  }
  
  describe() {
    // Extend parent's description
    return `${super.describe()} - Warrior Class`
  }
}

const hero = new Warrior("Aragorn")
console.log(hero.attack())    // "Aragorn attacks with a Sword!"
console.log(hero.describe())  // "Aragorn (150 HP) - Warrior Class"
Pattern: Extend, Don’t Replace. When overriding methods, consider calling super.method() first to preserve parent behavior, then add child-specific logic. This keeps your code DRY and ensures parent functionality isn’t accidentally lost.

Method Overriding

Method overriding occurs when a child class defines a method with the same name as one in its parent class. The child’s version “shadows” the parent’s version — when you call that method on a child instance, the child’s implementation runs.
class Character {
  attack() {
    return `${this.name} attacks!`
  }
}

class Warrior extends Character {
  attack() {
    return `${this.name} swings a mighty sword for 25 damage!`
  }
}

class Mage extends Character {
  attack() {
    return `${this.name} hurls a fireball for 30 damage!`
  }
}

class Archer extends Character {
  attack() {
    return `${this.name} fires an arrow for 20 damage!`
  }
}

// Each class has the SAME method name, but DIFFERENT behavior
const warrior = new Warrior("Conan")
const mage = new Mage("Gandalf")
const archer = new Archer("Legolas")

console.log(warrior.attack())  // "Conan swings a mighty sword for 25 damage!"
console.log(mage.attack())     // "Gandalf hurls a fireball for 30 damage!"
console.log(archer.attack())   // "Legolas fires an arrow for 20 damage!"

Why Override Methods?

ReasonExample
SpecializationEach character type attacks differently
ExtensionAdd logging before calling super.method()
CustomizationChange default values or behavior
PerformanceOptimize for specific use case

Extending vs Replacing

You have two choices when overriding:
class Warrior extends Character {
  // Completely new implementation
  attack() {
    this.rage += 5
    const damage = 20 + this.rage
    return `${this.name} rages and deals ${damage} damage!`
  }
}

What is Polymorphism?

Polymorphism (from Greek: “many forms”) means that objects of different types can be treated through a common interface. In JavaScript, this primarily manifests as subtype polymorphism: child class instances can be used wherever a parent class instance is expected. The magic happens when you call the same method on different objects, and each responds in its own way:
class Character {
  constructor(name) {
    this.name = name
    this.health = 100
  }
  
  attack() {
    return `${this.name} attacks!`
  }
}

class Warrior extends Character {
  attack() {
    return `${this.name} swings a sword!`
  }
}

class Mage extends Character {
  attack() {
    return `${this.name} casts a spell!`
  }
}

class Archer extends Character {
  attack() {
    return `${this.name} shoots an arrow!`
  }
}

// THE POLYMORPHISM POWER MOVE
// This function works with ANY Character type!
function executeBattle(characters) {
  console.log("⚔️ Battle begins!")
  
  characters.forEach(char => {
    // Each character attacks in their OWN way
    console.log(char.attack())
  })
}

// Mix of different types — polymorphism in action!
const party = [
  new Warrior("Conan"),
  new Mage("Gandalf"),
  new Archer("Legolas"),
  new Character("Villager")  // Even the base class works!
]

executeBattle(party)
// ⚔️ Battle begins!
// "Conan swings a sword!"
// "Gandalf casts a spell!"
// "Legolas shoots an arrow!"
// "Villager attacks!"

Why Polymorphism is Powerful

┌─────────────────────────────────────────────────────────────────────────┐
│                    POLYMORPHISM: WRITE ONCE, USE MANY                    │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   WITHOUT Polymorphism              WITH Polymorphism                    │
│   ─────────────────────             ─────────────────                    │
│                                                                          │
│   function battle(char) {           function battle(char) {              │
│     if (char instanceof Warrior) {    char.attack()  // That's it!       │
│       char.swingSword()             }                                    │
│     } else if (char instanceof      // Works with Warrior, Mage,         │
│       Mage) {                       // Archer, and ANY future type!      │
│       char.castSpell()                                                   │
│     } else if (char instanceof                                           │
│       Archer) {                                                          │
│       char.shootArrow()                                                  │
│     }                                                                    │
│     // Need to add code for                                              │
│     // every new character type!                                         │
│   }                                                                      │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘
BenefitExplanation
Open for ExtensionAdd new character types without changing battle logic
Loose CouplingexecuteBattle doesn’t need to know about specific types
Cleaner CodeNo endless if/else or switch statements
Easier TestingTest with mock objects that share the interface

The instanceof Operator

Use instanceof to check if an object is an instance of a class (or its parents):
const warrior = new Warrior("Conan")

console.log(warrior instanceof Warrior)    // true (direct)
console.log(warrior instanceof Character)  // true (parent)
console.log(warrior instanceof Object)     // true (all objects)
console.log(warrior instanceof Mage)       // false (different branch)

Under the Hood: Prototypes

Here’s a secret: ES6 class and extends are syntactic sugar over JavaScript’s prototype-based inheritance. When you write class Warrior extends Character, JavaScript is really setting up a prototype chain behind the scenes.
// What you write (ES6 class syntax)
class Character {
  constructor(name) {
    this.name = name
  }
  attack() {
    return `${this.name} attacks!`
  }
}

// Note: In this example, Warrior does NOT override attack()
// This lets us see how the prototype chain lookup works
class Warrior extends Character {
  constructor(name) {
    super(name)
    this.rage = 0
  }
  
  // Warrior-specific method (not on Character)
  battleCry() {
    return `${this.name} roars!`
  }
}

// What JavaScript actually creates (simplified)
// Warrior.prototype.__proto__ === Character.prototype
When you call warrior.attack(), JavaScript walks up the prototype chain:
  1. Looks for attack on the warrior instance itself — not found
  2. Looks on Warrior.prototype — not found (Warrior didn’t override it)
  3. Follows the chain to Character.prototypefound! Executes it
This is why inheritance “just works” — methods defined on parent classes are automatically available to child instances through the prototype chain.
┌─────────────────────────────────────────────────────────────────────────┐
│                          PROTOTYPE CHAIN                                 │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   warrior (instance)                                                     │
│   ┌─────────────────┐                                                   │
│   │ name: "Conan"   │                                                   │
│   │ rage: 0         │                                                   │
│   │ [[Prototype]] ──┼──┐                                                │
│   └─────────────────┘  │                                                │
│                        ▼                                                │
│   Warrior.prototype    ┌─────────────────┐                              │
│                        │ battleCry()     │                              │
│                        │ constructor     │                              │
│                        │ [[Prototype]] ──┼──┐                           │
│                        └─────────────────┘  │                           │
│                                             ▼                           │
│   Character.prototype  ┌─────────────────┐                              │
│                        │ attack()        │ ← Found here!                │
│                        │ constructor     │                              │
│                        │ [[Prototype]] ──┼──► Object.prototype          │
│                        └─────────────────┘                              │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘
Rule of Thumb: Use ES6 class syntax for cleaner, more readable code. Understand prototypes for debugging and advanced patterns. For a deep dive, see our Object Creation & Prototypes guide.

Composition vs Inheritance

Inheritance is powerful, but it’s not always the right tool. There’s a famous saying in programming:
“You wanted a banana but got a gorilla holding the banana and the entire jungle.”
This is the Gorilla-Banana Problem — when you inherit from a class, you inherit everything, even the stuff you don’t need.

When Inheritance Goes Wrong

// Inheritance nightmare — deep, rigid hierarchy
class Animal { }
class Mammal extends Animal { }
class WingedMammal extends Mammal { }
class Bat extends WingedMammal { }

// Oh no! Now we need a FlyingFish...
// Fish aren't mammals! Do we create another branch?
// What about a Penguin (bird that can't fly)?

// The hierarchy becomes fragile and hard to change

The “IS-A” vs “HAS-A” Test

QuestionIf Yes…Example
Is a Warrior a type of Character?Use inheritanceclass Warrior extends Character
Does a Character have inventory?Use compositionthis.inventory = new Inventory()

Composition: Building with “HAS-A”

Instead of inheriting behavior, you compose objects from smaller, reusable pieces:
// Rigid hierarchy — what if we need a flying warrior?
class Character { }
class FlyingCharacter extends Character {
  fly() { return `${this.name} flies!` }
}
class MagicCharacter extends Character {
  castSpell() { return `${this.name} casts!` }
}
// Can't have a character that BOTH flies AND casts!

When to Use Each

┌─────────────────────────────────────────────────────────────────────────┐
│                     INHERITANCE vs COMPOSITION                           │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   Use INHERITANCE when:              Use COMPOSITION when:               │
│   ─────────────────────              ────────────────────                │
│                                                                          │
│   • Clear "IS-A" relationship        • "HAS-A" relationship              │
│     (Warrior IS-A Character)           (Character HAS inventory)         │
│                                                                          │
│   • Child uses MOST of parent's      • Only need SOME behaviors          │
│     functionality                                                        │
│                                                                          │
│   • Hierarchy is shallow             • Behaviors need to be mixed        │
│     (2-3 levels max)                   freely                            │
│                                                                          │
│   • Relationships are stable         • Requirements change frequently    │
│     and unlikely to change                                               │
│                                                                          │
│   • You control the parent class     • Inheriting from 3rd party code    │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘
The Rule of Thumb: “Favor composition over inheritance.” Start with composition. Only use inheritance when you have a clear, stable “IS-A” relationship and the child truly needs most of the parent’s behavior.

Mixins: Sharing Behavior Without Inheritance

Mixins provide a way to add functionality to classes without using inheritance. They’re like a toolkit of behaviors you can “mix in” to any class.

Basic Mixin Pattern

// Define behaviors as objects
const Swimmer = {
  swim() {
    return `${this.name} swims through the water!`
  }
}

const Flyer = {
  fly() {
    return `${this.name} soars through the sky!`
  }
}

const Walker = {
  walk() {
    return `${this.name} walks on land!`
  }
}

// A base class
class Animal {
  constructor(name) {
    this.name = name
  }
}

// Mix behaviors into classes as needed
class Duck extends Animal { }
Object.assign(Duck.prototype, Swimmer, Flyer, Walker)

class Fish extends Animal { }
Object.assign(Fish.prototype, Swimmer)

class Eagle extends Animal { }
Object.assign(Eagle.prototype, Flyer, Walker)

// Use them!
const donald = new Duck("Donald")
console.log(donald.swim())  // "Donald swims through the water!"
console.log(donald.fly())   // "Donald soars through the sky!"
console.log(donald.walk())  // "Donald walks on land!"

const nemo = new Fish("Nemo")
console.log(nemo.swim())    // "Nemo swims through the water!"
// nemo.fly()               // ❌ Error: fly is not a function

Functional Mixin Pattern

A cleaner approach uses functions that take a class and return an enhanced class:
// Mixins as functions that enhance classes
const withLogging = (Base) => class extends Base {
  log(message) {
    console.log(`[${this.name}]: ${message}`)
  }
}

const withTimestamp = (Base) => class extends Base {
  getTimestamp() {
    return new Date().toISOString()
  }
}

// Apply mixins by wrapping the class
class Character {
  constructor(name) {
    this.name = name
  }
}

// Stack multiple mixins!
class LoggedCharacter extends withTimestamp(withLogging(Character)) {
  doAction() {
    this.log(`Action performed at ${this.getTimestamp()}`)
  }
}

const hero = new LoggedCharacter("Aragorn")
hero.doAction()  // "[Aragorn]: Action performed at 2024-01-15T..."

When to Use Mixins

Use CaseExample
Cross-cutting concernsLogging, serialization, event handling
Multiple behaviors neededA class that needs swimming AND flying
Third-party class extensionAdding methods to classes you don’t control
Avoiding deep hierarchiesInstead of FlyingSwimmingWalkingAnimal
Mixin Gotchas:
  • Name collisions: If two mixins define the same method, one overwrites the other
  • “this” confusion: Mixins must work with whatever this they’re mixed into
  • Hidden dependencies: Mixins might expect certain properties to exist
  • Debugging difficulty: Hard to trace where methods come from

Common Mistakes

1. Forgetting to Call super() in Constructor

// ❌ WRONG — ReferenceError!
class Warrior extends Character {
  constructor(name) {
    this.rage = 0  // Error: must call super first!
    super(name)
  }
}

// ✓ CORRECT — super() first, always
class Warrior extends Character {
  constructor(name) {
    super(name)    // FIRST!
    this.rage = 0  // Now this is safe
  }
}

2. Using this Before super()

// ❌ WRONG — Can't use 'this' until super() is called
class Mage extends Character {
  constructor(name, mana) {
    this.mana = mana  // ReferenceError!
    super(name)
  }
}

// ✓ CORRECT
class Mage extends Character {
  constructor(name, mana) {
    super(name)
    this.mana = mana  // Works now!
  }
}

3. Deep Inheritance Hierarchies

// ❌ BAD — Too deep, too fragile
class Entity { }
class LivingEntity extends Entity { }
class Animal extends LivingEntity { }
class Mammal extends Animal { }
class Canine extends Mammal { }
class Dog extends Canine { }
class Labrador extends Dog { }  // 7 levels deep! 😱

// ✓ BETTER — Keep it shallow, use composition
class Dog {
  constructor(breed) {
    this.breed = breed
    this.behaviors = {
      ...canWalk,
      ...canBark,
      ...canFetch
    }
  }
}

4. Inheriting Just for Code Reuse

// ❌ WRONG — Stack is NOT an Array (violates IS-A)
class Stack extends Array {
  peek() { return this[this.length - 1] }
}

const stack = new Stack()
stack.push(1, 2, 3)
stack.shift()  // 😱 Stacks shouldn't allow this!

// ✓ CORRECT — Stack HAS-A array (composition)
class Stack {
  #items = []
  
  push(item) { this.#items.push(item) }
  pop() { return this.#items.pop() }
  peek() { return this.#items[this.#items.length - 1] }
}

Inheritance Decision Flowchart

┌─────────────────────────────────────────────────────────────────────────┐
│                    SHOULD I USE INHERITANCE?                             │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│    Is it an "IS-A" relationship?                                         │
│    (A Warrior IS-A Character?)                                           │
│            │                                                             │
│       YES  │  NO                                                         │
│            │   └──────► Use COMPOSITION ("HAS-A")                        │
│            ▼                                                             │
│    Will child use MOST of parent's methods?                              │
│            │                                                             │
│       YES  │  NO                                                         │
│            │   └──────► Use COMPOSITION or MIXINS                        │
│            ▼                                                             │
│    Is hierarchy shallow (≤3 levels)?                                     │
│            │                                                             │
│       YES  │  NO                                                         │
│            │   └──────► REFACTOR! Flatten with composition               │
│            ▼                                                             │
│       Use INHERITANCE ✓                                                  │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Classic Interview Questions

Inheritance establishes an “IS-A” relationship where a child class inherits all properties and methods from a parent class. It creates a tight coupling between classes.Composition establishes a “HAS-A” relationship where a class contains instances of other classes to reuse their functionality. It provides more flexibility and loose coupling.
// Inheritance: Warrior IS-A Character
class Warrior extends Character { }

// Composition: Character HAS-A weapon
class Character {
  constructor() {
    this.weapon = new Sword()  // HAS-A
  }
}
Rule of thumb: Favor composition for flexibility, use inheritance for true type hierarchies.
Polymorphism means “many forms” — the ability for different objects to respond to the same method call in different ways.
class Shape {
  area() { return 0 }
}

class Rectangle extends Shape {
  constructor(w, h) { super(); this.w = w; this.h = h }
  area() { return this.w * this.h }
}

class Circle extends Shape {
  constructor(r) { super(); this.r = r }
  area() { return Math.PI * this.r ** 2 }
}

// Polymorphism in action — same method, different results
const shapes = [new Rectangle(4, 5), new Circle(3)]
shapes.forEach(s => console.log(s.area()))
// 20
// 28.274...
The area() method works differently based on the actual object type, but we can treat all shapes uniformly.
super has two main uses:
  1. super() — Calls the parent class constructor (required in child constructors before using this)
  2. super.method() — Calls a method from the parent class
class Parent {
  constructor(name) { this.name = name }
  greet() { return `Hello, I'm ${this.name}` }
}

class Child extends Parent {
  constructor(name, age) {
    super(name)  // Call parent constructor
    this.age = age
  }
  
  greet() {
    return `${super.greet()} and I'm ${this.age}`  // Call parent method
  }
}
Deep hierarchies (more than 3 levels) create several problems:
  1. Fragile Base Class Problem: Changes to a parent class can break many descendants
  2. Tight Coupling: Child classes become dependent on implementation details
  3. Inflexibility: Hard to reuse code outside the hierarchy
  4. Complexity: Difficult to understand and debug method resolution
  5. The Gorilla-Banana Problem: You inherit everything, even what you don’t need
Solution: Keep hierarchies shallow (2-3 levels max) and prefer composition for sharing behavior.
JavaScript uses prototype-based inheritance rather than class-based:
Classical OOP (Java, C++)JavaScript
Classes are blueprints”Classes” are functions with prototypes
Objects are instances of classesObjects inherit from other objects
Static class hierarchyDynamic prototype chain
Multiple inheritance via interfacesSingle prototype chain (use mixins for multiple)
ES6 class syntax is syntactic sugar — under the hood, it’s still prototypes:
class Dog extends Animal { }

// Is equivalent to setting up:
// Dog.prototype.__proto__ === Animal.prototype

Key Takeaways

Remember these essential points about Inheritance & Polymorphism:
  1. Inheritance lets child classes reuse parent code — use extends to create class hierarchies
  2. Always call super() first in child constructors — before using this
  3. super.method() calls the parent’s version — useful for extending rather than replacing behavior
  4. Method overriding = same name, different behavior — the child’s method shadows the parent’s
  5. Polymorphism = “many forms” — treat different object types through a common interface
  6. ES6 classes are syntactic sugar over prototypes — understand prototypes for debugging
  7. “IS-A” → inheritance, “HAS-A” → composition — use the right tool for the relationship
  8. The Gorilla-Banana problem is real — deep hierarchies inherit too much baggage
  9. Favor composition over inheritance — it’s more flexible and maintainable
  10. Keep inheritance hierarchies shallow — 2-3 levels maximum
  11. Mixins share behavior without inheritance chains — useful for cross-cutting concerns
  12. instanceof checks the entire prototype chainwarrior instanceof Character is true

Test Your Knowledge

Answer: JavaScript throws a ReferenceError with the message “Must call super constructor in derived class before accessing ‘this’ or returning from derived constructor”.
class Child extends Parent {
  constructor() {
    this.name = "test"  // ❌ ReferenceError!
  }
}
The super() call is mandatory because it initializes the parent part of the object, which must happen before the child can add its own properties.
Answer: Method overriding allows different classes to provide their own implementation of the same method name. This enables polymorphism because code can call that method on any object without knowing its specific type — each object responds appropriately.
function makeSound(animal) {
  console.log(animal.speak())  // Works with ANY animal type
}

class Dog { speak() { return "Woof!" } }
class Cat { speak() { return "Meow!" } }

makeSound(new Dog())  // "Woof!"
makeSound(new Cat())  // "Meow!"
Answer: Prefer composition when:
  • The relationship is “HAS-A” rather than “IS-A”
  • You only need some of the parent’s functionality
  • Behaviors need to be mixed freely (e.g., flying + swimming)
  • Requirements change frequently
  • You’re working with third-party code you don’t control
  • The inheritance hierarchy would exceed 3 levels
// Use composition: Character HAS abilities
class Character {
  constructor() {
    this.abilities = [canAttack, canDefend, canHeal]
  }
}
Answer: A mixin is a way to add functionality to classes without using inheritance. It’s an object (or function) containing methods that can be “mixed into” multiple classes.Use mixins for:
  • Cross-cutting concerns (logging, serialization)
  • When a class needs behaviors from multiple sources
  • Avoiding the diamond problem of multiple inheritance
const Serializable = {
  toJSON() { return JSON.stringify(this) }
}

class User { constructor(name) { this.name = name } }
Object.assign(User.prototype, Serializable)

new User("Alice").toJSON()  // '{"name":"Alice"}'
Answer: Use super.methodName() to call the parent’s version of an overridden method:
class Parent {
  greet() { return "Hello" }
}

class Child extends Parent {
  greet() {
    const parentGreeting = super.greet()  // "Hello"
    return `${parentGreeting} from Child!`
  }
}

new Child().greet()  // "Hello from Child!"
This is useful when you want to extend behavior rather than completely replace it.
Answer: The “IS-A” test determines if inheritance is appropriate by asking: “Is the child truly a specialized type of the parent?”
  • Passes: “A Warrior IS-A Character” ✓
  • Passes: “A Dog IS-A Animal” ✓
  • Fails: “A Stack IS-A Array” ✗ (Stack has different behavior)
  • Fails: “A Car IS-A Engine” ✗ (Car HAS-A Engine)
If it fails the IS-A test, use composition instead. This prevents the Liskov Substitution Principle violations where child instances can’t properly substitute for parent instances.

Frequently Asked Questions

Inheritance lets a class (child) reuse code from another class (parent) using the extends keyword. The child class inherits all methods and properties from the parent and can add or override them. Under the hood, JavaScript implements this through the prototype chain — the child’s prototype links to the parent’s prototype, as defined in the ECMAScript specification.
Polymorphism means different objects can respond to the same method call in their own way. When a Warrior and a Mage both have an attack() method but each behaves differently, that is polymorphism. It lets you treat different object types through a common interface, making code flexible and extensible without type-checking.
Inheritance creates “IS-A” relationships where child classes extend parents. Composition creates “HAS-A” relationships by combining smaller, focused objects. The Gang of Four Design Patterns book recommends favoring composition over inheritance because it avoids deep hierarchies, is easier to change, and lets you mix behaviors freely.
The extends keyword sets up the prototype chain so the child class inherits from the parent. It does two things: sets the child’s prototype to an object that delegates to the parent’s prototype, and ensures super() is called in the constructor to initialize the parent’s properties. Without super(), using this in the child constructor throws a ReferenceError.
This term, coined by Joe Armstrong, describes a flaw of deep inheritance: you wanted a banana but got a gorilla holding the banana and the entire jungle. It means that inheriting from a class forces you to inherit everything, including dependencies you do not need. The solution is to favor composition and mixins over deep class hierarchies.


Reference

Articles

Videos

Last modified on February 17, 2026