Learn inheritance and polymorphism in JavaScript. Extend classes, use prototype chains, override methods, and master OOP patterns.
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?
Copy
Ask AI
// One base class, infinite possibilitiesclass 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!
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
Copy
Ask AI
// The parent class — all characters share these basicsclass 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 automaticallyclass 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.
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.
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.
Copy
Ask AI
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:
Copy
Ask AI
class Warrior extends Character { constructor(name) { this.rage = 0 // ❌ ReferenceError: Must call super constructor first! super(name) }}
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:
Copy
Ask AI
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 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.
Copy
Ask AI
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 behaviorconst 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!"
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:
Copy
Ask AI
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!"
┌─────────────────────────────────────────────────────────────────────────┐│ 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! ││ } ││ │└─────────────────────────────────────────────────────────────────────────┘
Benefit
Explanation
Open for Extension
Add new character types without changing battle logic
Loose Coupling
executeBattle doesn’t need to know about specific types
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.
Copy
Ask AI
// 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 worksclass 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:
Looks for attack on the warrior instance itself — not found
Looks on Warrior.prototype — not found (Warrior didn’t override it)
Follows the chain to Character.prototype — found! Executes it
This is why inheritance “just works” — methods defined on parent classes are automatically available to child instances through the prototype chain.
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.
// Inheritance nightmare — deep, rigid hierarchyclass 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
Instead of inheriting behavior, you compose objects from smaller, reusable pieces:
Inheritance Approach
Composition Approach
Copy
Ask AI
// 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!
Copy
Ask AI
// Flexible behaviors — mix and match!const canFly = (state) => ({ fly() { return `${state.name} soars through the sky!` }})const canCast = (state) => ({ castSpell(spell) { return `${state.name} casts ${spell}!` }})const canFight = (state) => ({ attack() { return `${state.name} attacks!` }})// Create a flying mage — compose the behaviors you need!function createFlyingMage(name) { const state = { name, health: 100, mana: 50 } return { ...state, ...canFly(state), ...canCast(state), ...canFight(state) }}const merlin = createFlyingMage("Merlin")console.log(merlin.fly()) // "Merlin soars through the sky!"console.log(merlin.castSpell("Ice")) // "Merlin casts Ice!"console.log(merlin.attack()) // "Merlin attacks!"
┌─────────────────────────────────────────────────────────────────────────┐│ 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.
// Define behaviors as objectsconst 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 classclass Animal { constructor(name) { this.name = name }}// Mix behaviors into classes as neededclass 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
A cleaner approach uses functions that take a class and return an enhanced class:
Copy
Ask AI
// Mixins as functions that enhance classesconst 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 classclass 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..."
┌─────────────────────────────────────────────────────────────────────────┐│ 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 ✓ ││ │└─────────────────────────────────────────────────────────────────────────┘
What's the difference between inheritance and composition?
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.
Copy
Ask AI
// Inheritance: Warrior IS-A Characterclass Warrior extends Character { }// Composition: Character HAS-A weaponclass Character { constructor() { this.weapon = new Sword() // HAS-A }}
Rule of thumb: Favor composition for flexibility, use inheritance for true type hierarchies.
Explain polymorphism with an example
Polymorphism means “many forms” — the ability for different objects to respond to the same method call in different ways.
Copy
Ask AI
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 resultsconst 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.
What does the 'super' keyword do in JavaScript?
super has two main uses:
super() — Calls the parent class constructor (required in child constructors before using this)
super.method() — Calls a method from the parent class
1. What happens if you forget to call super() in a child constructor?
Answer: JavaScript throws a ReferenceError with the message “Must call super constructor in derived class before accessing ‘this’ or returning from derived constructor”.
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.
2. How does method overriding enable polymorphism?
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.
Copy
Ask AI
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!"
3. When should you prefer composition over inheritance?
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
Copy
Ask AI
// Use composition: Character HAS abilitiesclass Character { constructor() { this.abilities = [canAttack, canDefend, canHeal] }}
4. What's a mixin and when would you use one?
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
5. How can you call a parent's method from an overriding method?
Answer: Use super.methodName() to call the parent’s version of an overridden method:
Copy
Ask AI
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.
6. What's the 'IS-A' test for inheritance?
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.
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.
What is polymorphism in JavaScript?
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.
What is the difference between inheritance and composition in JavaScript?
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.
How does the extends keyword work in JavaScript?
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.
What is the Gorilla-Banana problem?
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.