docs/concepts/inheritance-polymorphism.mdx
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.
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:
// 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!)
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.
extendsThe extends keyword creates a class that is a child of another class. The syntax is straightforward:
class ChildClass extends ParentClass {
// Child-specific code here
}
```javascript
class Character {
constructor(name) {
this.name = name
this.health = 100
}
attack() {
return `${this.name} attacks!`
}
}
```
```javascript
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}!`
}
}
```
```javascript
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!"
```
When you use extends, the child class inherits:
| Inherited | Example |
|---|---|
| Instance properties | this.name, this.health |
| Instance methods | attack(), takeDamage() |
| Static methods | Character.createRandom() (if defined) |
| Getters/Setters | get 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)
super KeywordThe super keyword is your lifeline when working with inheritance. It has two main uses:
super() — Calling the Parent ConstructorWhen 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)
class Warrior extends Character {
constructor(name) {
this.rage = 0 // ❌ ReferenceError: Must call super constructor first!
super(name)
}
}
super.method() — Calling Parent MethodsUse 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"
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!"
| Reason | Example |
|---|---|
| Specialization | Each character type attacks differently |
| Extension | Add logging before calling super.method() |
| Customization | Change default values or behavior |
| Performance | Optimize for specific use case |
You have two choices when overriding:
<Tabs> <Tab title="Replace Completely"> ```javascript class Warrior extends Character { // Completely new implementation attack() { this.rage += 5 const damage = 20 + this.rage return `${this.name} rages and deals ${damage} damage!` } } ``` </Tab> <Tab title="Extend Parent"> ```javascript class Warrior extends Character { // Build on parent's behavior attack() { const base = super.attack() // "Conan attacks!" this.rage += 5 return `${base} Rage builds to ${this.rage}!` } } ``` </Tab> </Tabs>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!"
┌─────────────────────────────────────────────────────────────────────────┐
│ 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 |
| Cleaner Code | No endless if/else or switch statements |
| Easier Testing | Test with mock objects that share the interface |
instanceof OperatorUse 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)
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:
attack on the warrior instance itself — not foundWarrior.prototype — not found (Warrior didn't override it)Character.prototype — found! Executes itThis 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 │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
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.
// 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
| Question | If Yes... | Example |
|---|---|---|
| Is a Warrior a type of Character? | Use inheritance | class Warrior extends Character |
| Does a Character have inventory? | Use composition | this.inventory = new Inventory() |
Instead of inheriting behavior, you compose objects from smaller, reusable pieces:
<Tabs> <Tab title="Inheritance Approach"> ```javascript // 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! ``` </Tab> <Tab title="Composition Approach"> ```javascript // 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 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
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.
// 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
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..."
| Use Case | Example |
|---|---|
| Cross-cutting concerns | Logging, serialization, event handling |
| Multiple behaviors needed | A class that needs swimming AND flying |
| Third-party class extension | Adding methods to classes you don't control |
| Avoiding deep hierarchies | Instead of FlyingSwimmingWalkingAnimal |
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
}
}
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!
}
}
// ❌ 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
}
}
}
// ❌ 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] }
}
┌─────────────────────────────────────────────────────────────────────────┐
│ 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 ✓ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
**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.
```javascript
// 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.
```javascript
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.
1. **`super()`** — Calls the parent class constructor (required in child constructors before using `this`)
2. **`super.method()`** — Calls a method from the parent class
```javascript
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
}
}
```
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.
| Classical OOP (Java, C++) | JavaScript |
|---------------------------|------------|
| Classes are blueprints | "Classes" are functions with prototypes |
| Objects are instances of classes | Objects inherit from other objects |
| Static class hierarchy | Dynamic prototype chain |
| Multiple inheritance via interfaces | Single prototype chain (use mixins for multiple) |
ES6 `class` syntax is syntactic sugar — under the hood, it's still prototypes:
```javascript
class Dog extends Animal { }
// Is equivalent to setting up:
// Dog.prototype.__proto__ === Animal.prototype
```
Inheritance lets child classes reuse parent code — use extends to create class hierarchies
Always call super() first in child constructors — before using this
super.method() calls the parent's version — useful for extending rather than replacing behavior
Method overriding = same name, different behavior — the child's method shadows the parent's
Polymorphism = "many forms" — treat different object types through a common interface
ES6 classes are syntactic sugar over prototypes — understand prototypes for debugging
"IS-A" → inheritance, "HAS-A" → composition — use the right tool for the relationship
The Gorilla-Banana problem is real — deep hierarchies inherit too much baggage
Favor composition over inheritance — it's more flexible and maintainable
Keep inheritance hierarchies shallow — 2-3 levels maximum
Mixins share behavior without inheritance chains — useful for cross-cutting concerns
instanceof checks the entire prototype chain — warrior instanceof Character is true
```javascript
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.
```javascript
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!"
```
- 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
```javascript
// Use composition: Character HAS abilities
class Character {
constructor() {
this.abilities = [canAttack, canDefend, canHeal]
}
}
```
Use mixins for:
- Cross-cutting concerns (logging, serialization)
- When a class needs behaviors from multiple sources
- Avoiding the diamond problem of multiple inheritance
```javascript
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"}'
```
```javascript
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.
- **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.