Skip to main content

Class Inheritance in JavaScript

Inheritance lets you create new classes based on existing ones, reusing their code while adding or modifying behavior. In JavaScript, the extends keyword establishes an inheritance relationship between two classes, connecting them through the prototype chain. The child class (subclass) automatically gains access to all methods and properties of the parent class (superclass), and can override or extend them as needed.

This guide covers everything about class inheritance: how extends works, how to override methods, the rules around super, the internal [[HomeObject]] mechanism that makes it all work, how static members are inherited, and a complete visualization of the prototype chain that classes create.

The extends Keyword

The extends keyword creates a parent-child relationship between two classes. The child class inherits all methods from the parent class.

Basic Inheritance

class Animal {
constructor(name) {
this.name = name;
this.energy = 100;
}

eat(amount) {
this.energy += amount;
console.log(`${this.name} eats. Energy: ${this.energy}`);
}

sleep() {
this.energy += 20;
console.log(`${this.name} sleeps. Energy: ${this.energy}`);
}

move(distance) {
this.energy -= distance;
console.log(`${this.name} moves ${distance}m. Energy: ${this.energy}`);
}
}

class Dog extends Animal {
bark() {
console.log(`${this.name} says: Woof!`);
}
}

class Cat extends Animal {
purr() {
console.log(`${this.name} purrs...`);
}
}

const rex = new Dog("Rex");
rex.eat(30); // "Rex eats. Energy: 130" (inherited from Animal)
rex.move(10); // "Rex moves 10m. Energy: 120" (inherited from Animal)
rex.bark(); // "Rex says: Woof!" (defined on Dog)
rex.sleep(); // "Rex sleeps. Energy: 140" (inherited from Animal)

const whiskers = new Cat("Whiskers");
whiskers.eat(20); // "Whiskers eats. Energy: 120" (inherited from Animal)
whiskers.purr(); // "Whiskers purrs..." (defined on Cat)

// Dog does not have purr, Cat does not have bark
// rex.purr(); // TypeError: rex.purr is not a function
// whiskers.bark(); // TypeError: whiskers.bark is not a function

Dog and Cat each inherit eat, sleep, and move from Animal without rewriting any of that code. Each subclass adds its own unique methods.

What extends Does Behind the Scenes

When you write class Dog extends Animal, JavaScript sets up two prototype links:

  1. Dog.prototype.__proto__ is set to Animal.prototype (so instances of Dog can access Animal's methods)
  2. Dog.__proto__ is set to Animal (so Dog inherits static methods from Animal)
// Instance prototype chain
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true

// Constructor prototype chain (for statics)
console.log(Object.getPrototypeOf(Dog) === Animal); // true

// Instance checks
const rex = new Dog("Rex");
console.log(rex instanceof Dog); // true
console.log(rex instanceof Animal); // true
console.log(rex instanceof Object); // true

extends Works with Any Expression

The expression after extends does not have to be a simple class name. It can be any expression that evaluates to a constructor:

// Extending from a function call
function createBaseClass(greeting) {
return class {
greet() {
console.log(`${greeting}, I'm ${this.name}`);
}
};
}

class FormalUser extends createBaseClass("Good day") {
constructor(name) {
super();
this.name = name;
}
}

class CasualUser extends createBaseClass("Hey") {
constructor(name) {
super();
this.name = name;
}
}

new FormalUser("Alice").greet(); // "Good day, I'm Alice"
new CasualUser("Bob").greet(); // "Hey, I'm Bob"

This technique is powerful for mixins and dynamic inheritance, which we will cover in later articles.

Overriding Methods

A child class can override a parent method by defining a method with the same name. The child's version completely replaces the parent's version for instances of the child class.

Simple Override

class Animal {
constructor(name) {
this.name = name;
}

speak() {
console.log(`${this.name} makes a generic sound.`);
}

toString() {
return `Animal: ${this.name}`;
}
}

class Dog extends Animal {
// Override the speak method
speak() {
console.log(`${this.name} barks!`);
}

// Override toString
toString() {
return `Dog: ${this.name}`;
}
}

class Cat extends Animal {
speak() {
console.log(`${this.name} meows!`);
}
}

const animal = new Animal("Generic");
const dog = new Dog("Rex");
const cat = new Cat("Whiskers");

animal.speak(); // "Generic makes a generic sound."
dog.speak(); // "Rex barks!"
cat.speak(); // "Whiskers meows!"

console.log(`${dog}`); // "Dog: Rex"
console.log(`${cat}`); // "Animal: Whiskers" (Cat didn't override toString)

When dog.speak() is called, JavaScript looks for speak on the Dog.prototype first. It finds it there and uses it, never reaching Animal.prototype.

Polymorphism in Practice

Overriding enables polymorphism: different classes responding to the same method call in different ways:

class Shape {
area() {
throw new Error("area() must be implemented by subclass");
}

describe() {
return `${this.constructor.name} with area ${this.area().toFixed(2)}`;
}
}

class Circle extends Shape {
constructor(radius) {
super();
this.radius = radius;
}

area() {
return Math.PI * this.radius ** 2;
}
}

class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}

area() {
return this.width * this.height;
}
}

class Triangle extends Shape {
constructor(base, height) {
super();
this.base = base;
this.height = height;
}

area() {
return 0.5 * this.base * this.height;
}
}

// Polymorphism: same interface, different behavior
const shapes = [
new Circle(5),
new Rectangle(4, 6),
new Triangle(3, 8)
];

shapes.forEach(shape => {
console.log(shape.describe());
});
// "Circle with area 78.54"
// "Rectangle with area 24.00"
// "Triangle with area 12.00"

Each shape overrides area() with its own formula. The describe() method in the parent calls this.area(), which dispatches to the correct override based on the actual type of the object.

The super Keyword: Calling Parent Methods

Often, you do not want to completely replace a parent method. You want to extend it by adding behavior before or after the parent's logic. The super keyword lets you call the parent class's methods from within the child class.

Calling Parent Methods with super.method()

class Animal {
constructor(name) {
this.name = name;
}

speak() {
return `${this.name} makes a sound`;
}

eat(food) {
console.log(`${this.name} eats ${food}`);
this.lastMeal = food;
}
}

class Dog extends Animal {
speak() {
// Call the parent's speak method, then add to it
const parentResult = super.speak();
return `${parentResult}... specifically, a bark!`;
}

eat(food) {
// Add behavior before calling parent
console.log(`${this.name} wags tail excitedly`);

// Call the parent's eat method
super.eat(food);

// Add behavior after calling parent
console.log(`${this.name} is happy!`);
}
}

const rex = new Dog("Rex");

console.log(rex.speak());
// "Rex makes a sound... specifically, a bark!"

rex.eat("bone");
// "Rex wags tail excitedly"
// "Rex eats bone"
// "Rex is happy!"

super in Arrow Functions

Arrow functions do not have their own super. They inherit it from the enclosing method, just like this:

class Animal {
speak() {
return "Animal speaks";
}
}

class Dog extends Animal {
speak() {
// Arrow function correctly inherits "super" from the speak() method
const getParent = () => super.speak();
return `${getParent()} → Dog barks`;
}
}

console.log(new Dog().speak()); // "Animal speaks → Dog barks"

Real-World Example: Extending a Logger

class Logger {
log(message) {
console.log(`[LOG] ${message}`);
}

error(message) {
console.error(`[ERROR] ${message}`);
}
}

class TimestampLogger extends Logger {
log(message) {
const timestamp = new Date().toISOString();
super.log(`${timestamp} - ${message}`);
}

error(message) {
const timestamp = new Date().toISOString();
super.error(`${timestamp} - ${message}`);
}
}

class FileLogger extends TimestampLogger {
constructor(filename) {
super();
this.filename = filename;
this.entries = [];
}

log(message) {
// Still get the timestamp behavior from TimestampLogger
super.log(message);
// Also store for later file writing
this.entries.push({ type: "log", message, time: new Date() });
}
}

const logger = new FileLogger("app.log");
logger.log("Server started");
// [LOG] 2026-01-15T10:30:00.000Z - Server started
console.log(logger.entries.length); // 1

Each level adds its own behavior while preserving the parent's behavior through super.

Overriding the Constructor: The super() Requirement

Overriding the constructor in a child class has a strict rule: you must call super() before using this. This is not optional; failing to do so throws a ReferenceError.

The Rule: super() Before this

class Animal {
constructor(name) {
this.name = name;
}
}

class Dog extends Animal {
constructor(name, breed) {
// MUST call super() before accessing "this"
super(name); // Calls Animal's constructor
this.breed = breed; // Now "this" is available
}
}

const rex = new Dog("Rex", "German Shepherd");
console.log(rex.name); // "Rex" (set by Animal's constructor)
console.log(rex.breed); // "German Shepherd" (set by Dog's constructor)

What Happens Without super()

class Dog extends Animal {
constructor(name, breed) {
// Trying to use "this" before super()
// this.breed = breed; // ReferenceError: Must call super constructor before accessing 'this'

super(name);
this.breed = breed; // This is fine (after super())
}
}

Why super() Is Required

In regular (non-derived) classes, new creates an empty object and sets it as this. But in derived classes (classes with extends), the parent constructor is responsible for creating the object. The child constructor must delegate to the parent via super() before this exists.

The execution flow is:

  1. new Dog("Rex", "Shepherd") is called
  2. JavaScript sees Dog extends Animal, so it does NOT create an object yet
  3. Dog's constructor runs, and it MUST call super(name)
  4. super(name) calls Animal's constructor, which creates the object and sets this.name
  5. Control returns to Dog's constructor, where this is now the created object
  6. Dog's constructor can now set this.breed

Default Constructor for Derived Classes

If you do not define a constructor in a child class, JavaScript automatically provides one that passes all arguments to the parent:

class Animal {
constructor(name) {
this.name = name;
}
}

class Dog extends Animal {
// No constructor defined, JavaScript generates:
// constructor(...args) {
// super(...args);
// }

bark() {
console.log(`${this.name} barks!`);
}
}

const rex = new Dog("Rex");
console.log(rex.name); // "Rex" (passed through to Animal's constructor)
rex.bark(); // "Rex barks!"

This automatic constructor is why simple subclasses that only add methods (without extra properties) do not need an explicit constructor.

Common Mistakes with super()

Mistake 1: Forgetting super() entirely

class Dog extends Animal {
constructor(name, breed) {
// Missing super() call
this.breed = breed;
}
}

// new Dog("Rex", "Husky");
// ReferenceError: Must call super constructor in derived class
// before accessing 'this' or returning from derived constructor

Mistake 2: Using this before super()

class Dog extends Animal {
constructor(name, breed) {
this.breed = breed; // ReferenceError! "this" doesn't exist yet
super(name);
}
}

Mistake 3: Conditionally calling super()

class Dog extends Animal {
constructor(name, breed) {
// WRONG: super() must be called in ALL code paths
if (breed) {
super(name);
this.breed = breed;
}
// What if breed is falsy? super() is never called!
}
}

The correct pattern is always to call super() unconditionally, then handle optional logic afterward:

class Dog extends Animal {
constructor(name, breed) {
super(name); // Always call super first
this.breed = breed || "Mixed";
}
}

Overriding with Complex Initialization

class Component {
constructor(selector) {
this.element = document.querySelector(selector);
this.state = {};
this.init();
}

init() {
console.log("Component initialized");
}

setState(newState) {
this.state = { ...this.state, ...newState };
this.render();
}

render() {
// Base render: does nothing
}
}

class Counter extends Component {
constructor(selector, initialCount = 0) {
super(selector); // Must come first
this.setState({ count: initialCount });
}

init() {
// Override init: called during super() execution
super.init();
console.log("Counter initialized");
}

increment() {
this.setState({ count: this.state.count + 1 });
}

render() {
if (this.element) {
this.element.textContent = `Count: ${this.state.count}`;
}
}
}
warning

Be careful with methods called from the parent constructor (like init() in the example above). When super() calls the parent constructor, and the parent constructor calls an overridden method, the child's version runs. But the child's constructor has not finished yet, so class fields specific to the child may not be initialized. This is a well-known pitfall in class hierarchies.

[[HomeObject]] and Method Borrowing

For super to work correctly, JavaScript needs to know which class's prototype to look at when resolving super.method(). This is tracked through an internal property called [[HomeObject]].

What Is [[HomeObject]]?

Every method defined in a class (or in an object literal using the shorthand syntax) has a hidden [[HomeObject]] property that permanently points to the object the method was defined in.

When you call super.method(), JavaScript:

  1. Looks at the [[HomeObject]] of the current method
  2. Goes to [[HomeObject]].__proto__ (the parent's prototype)
  3. Finds method there and calls it
class Animal {
speak() {
return "Animal speaks";
}
}

class Dog extends Animal {
speak() {
// [[HomeObject]] of this speak() is Dog.prototype
// super.speak() looks at Dog.prototype.__proto__ → Animal.prototype
// Finds speak() there
return super.speak() + " → Dog barks";
}
}

class GoldenRetriever extends Dog {
speak() {
// [[HomeObject]] of this speak() is GoldenRetriever.prototype
// super.speak() looks at GoldenRetriever.prototype.__proto__ → Dog.prototype
// Finds speak() there (which itself calls super.speak())
return super.speak() + " → Golden retriever woofs gently";
}
}

console.log(new GoldenRetriever().speak());
// "Animal speaks → Dog barks → Golden retriever woofs gently"

Why [[HomeObject]] Matters: The Infinite Recursion Problem

Without [[HomeObject]], using this.__proto__ for super would cause infinite recursion in multi-level inheritance:

// Imagine if super.speak() was implemented as this.__proto__.speak.call(this)
// In a chain A → B → C, calling C's speak() would:
// 1. this.__proto__ → B.prototype → call B's speak() with this = C instance
// 2. B's speak does this.__proto__ → but "this" is still the C instance!
// So this.__proto__ → B.prototype again (not A.prototype!)
// 3. B's speak is called again → infinite loop!

[[HomeObject]] solves this because it is fixed to the definition site, not the calling object.

[[HomeObject]] Is Only Set for Shorthand Methods

Only methods defined with the shorthand syntax (in classes or object literals) get [[HomeObject]]. Regular function properties do not:

const parent = {
// Shorthand method: HAS [[HomeObject]]
greet() {
return "Parent greets";
}
};

const child = {
__proto__: parent,

// Shorthand method: HAS [[HomeObject]] (super works)
greet() {
return super.greet() + " → Child greets";
}
};

console.log(child.greet()); // "Parent greets → Child greets"

const childBroken = {
__proto__: parent,

// Function property: NO [[HomeObject]] (super will NOT work)
greet: function() {
// return super.greet(); // SyntaxError: 'super' keyword unexpected here
return "broken";
}
};

Method Borrowing and [[HomeObject]] Limitations

Because [[HomeObject]] is permanently tied to the method's original definition location, borrowing methods between objects can cause unexpected super behavior:

class Animal {
speak() {
return "Animal";
}
}

class Dog extends Animal {
speak() {
return `${super.speak()} → Dog`;
}
}

class Cat extends Animal {
speak() {
return `${super.speak()} → Cat`;
}
}

const dog = new Dog();
const cat = new Cat();

console.log(dog.speak()); // "Animal → Dog"
console.log(cat.speak()); // "Animal → Cat"

// Borrow Dog's speak and put it on Cat's prototype
Cat.prototype.speak = Dog.prototype.speak;

console.log(cat.speak()); // "Animal → Dog" (not "Animal → Cat"!)
// Because speak's [[HomeObject]] is still Dog.prototype
// super.speak() goes to Dog.prototype.__proto__ → Animal.prototype
// The result is correct (Animal), but the method says "Dog" because
// the function body hasn't changed
info

Methods with super are not freely transferable between classes. Their [[HomeObject]] is fixed at definition time, so super will always resolve relative to the original class, not the class the method is called on. Avoid copying methods that use super between unrelated classes.

Inheriting Static Methods and Properties

extends not only sets up inheritance for instances (through prototypes) but also for static methods and properties. The child class constructor inherits static members from the parent class constructor.

Static Method Inheritance

class Animal {
constructor(name) {
this.name = name;
}

// Static method: belongs to the class itself
static create(name) {
return new this(name);
}

static compare(a, b) {
return a.name.localeCompare(b.name);
}
}

class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}

bark() {
console.log(`${this.name} barks!`);
}
}

// Dog inherits static methods from Animal
const rex = Dog.create("Rex");
console.log(rex instanceof Dog); // true
console.log(rex.name); // "Rex"
// Note: rex.breed is undefined because create() only passes name

// Using inherited static method
const dogs = [new Dog("Rex"), new Dog("Buddy"), new Dog("Alpha")];
dogs.sort(Animal.compare); // Using parent's static method
dogs.sort(Dog.compare); // Same method, inherited by Dog
console.log(dogs.map(d => d.name)); // ["Alpha", "Buddy", "Rex"]

Notice static create(name) uses new this(name). When called as Dog.create("Rex"), this inside the static method is Dog (the class itself), so it creates a Dog instance.

Static Property Inheritance

class Vehicle {
static category = "transport";
static count = 0;

constructor(make, model) {
this.make = make;
this.model = model;
Vehicle.count++;
}

static getCount() {
return Vehicle.count;
}
}

class Car extends Vehicle {
static wheels = 4;

constructor(make, model, doors) {
super(make, model);
this.doors = doors;
}
}

class Motorcycle extends Vehicle {
static wheels = 2;
}

// Static properties are inherited
console.log(Car.category); // "transport" (inherited from Vehicle)
console.log(Motorcycle.category); // "transport" (inherited from Vehicle)
console.log(Car.wheels); // 4 (defined on Car)
console.log(Motorcycle.wheels); // 2 (defined on Motorcycle)

Overriding Static Methods

Child classes can override static methods just like instance methods:

class Model {
static tableName() {
return "models";
}

static findAll() {
console.log(`SELECT * FROM ${this.tableName()}`);
}
}

class User extends Model {
static tableName() {
return "users"; // Override
}
}

class Product extends Model {
static tableName() {
return "products"; // Override
}
}

User.findAll(); // "SELECT * FROM users"
Product.findAll(); // "SELECT * FROM products"
Model.findAll(); // "SELECT * FROM models"

The static method findAll() calls this.tableName(), and this refers to the class on which findAll is called. Polymorphism works for static methods just as it does for instance methods.

How Static Inheritance Works Internally

The key is the prototype chain between the constructors themselves:

console.log(Object.getPrototypeOf(Dog) === Animal);                 // true
console.log(Object.getPrototypeOf(Animal) === Function.prototype); // true

When you access Dog.create, JavaScript:

  1. Looks for create on Dog itself: not found
  2. Follows Dog.__proto__ to Animal: found! Uses Animal.create

This is exactly the same prototype chain mechanism used for instance methods, just applied to the constructor functions themselves.

The Prototype Chain with Classes (Visual Diagram)

Understanding the complete prototype chain created by class inheritance ties everything together. Let's trace every link for this hierarchy:

class Animal {
constructor(name) {
this.name = name;
}

eat() {
return `${this.name} eats`;
}

static kingdom = "Animalia";

static create(name) {
return new this(name);
}
}

class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}

bark() {
return `${this.name} barks`;
}

static domesticated = true;
}

const rex = new Dog("Rex", "Husky");

The Complete Chain

INSTANCE PROTOTYPE CHAIN (for methods):
=========================================

rex (instance)
├── name: "Rex" (own property)
├── breed: "Husky" (own property)
└── [[Prototype]] ──→ Dog.prototype
├── bark() (Dog's method)
├── constructor ──→ Dog
└── [[Prototype]] ──→ Animal.prototype
├── eat() (Animal's method)
├── constructor ──→ Animal
└── [[Prototype]] ──→ Object.prototype
├── toString()
├── hasOwnProperty()
├── valueOf()
└── [[Prototype]] ──→ null


CONSTRUCTOR (STATIC) PROTOTYPE CHAIN (for static methods):
==========================================================

Dog (constructor function)
├── domesticated: true (own static property)
├── prototype ──→ Dog.prototype (see above)
└── [[Prototype]] ──→ Animal (constructor function)
├── kingdom: "Animalia" (own static property)
├── create() (own static method)
├── prototype ──→ Animal.prototype (see above)
└── [[Prototype]] ──→ Function.prototype
├── call()
├── apply()
├── bind()
└── [[Prototype]] ──→ Object.prototype
└── [[Prototype]] ──→ null
// Instance chain
console.log(Object.getPrototypeOf(rex) === Dog.prototype); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
console.log(Object.getPrototypeOf(Animal.prototype) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype) === null); // true

// Constructor (static) chain
console.log(Object.getPrototypeOf(Dog) === Animal); // true
console.log(Object.getPrototypeOf(Animal) === Function.prototype); // true

// instanceof checks (walks the instance chain)
console.log(rex instanceof Dog); // true
console.log(rex instanceof Animal); // true
console.log(rex instanceof Object); // true

// Method resolution
console.log(rex.bark()); // "Rex barks" (found on Dog.prototype)
console.log(rex.eat()); // "Rex eats" (found on Animal.prototype)
console.log(rex.toString()); // "[object Object]" (found on Object.prototype)

// Static resolution
console.log(Dog.domesticated); // true (found on Dog)
console.log(Dog.kingdom); // "Animalia" (found on Animal (via static chain))
console.log(Dog.create); // ƒ create() (found on Animal (via static chain))

How Method Lookup Works

When you call rex.eat():

  1. JavaScript checks rex (the instance) for an eat property: not found
  2. Follows rex.[[Prototype]] to Dog.prototype: no eat there
  3. Follows Dog.prototype.[[Prototype]] to Animal.prototype: eat found!
  4. Calls eat with this = rex

When you call Dog.kingdom:

  1. JavaScript checks Dog for a kingdom property: not found
  2. Follows Dog.[[Prototype]] to Animal: kingdom found!
  3. Returns "Animalia"

Multi-Level Inheritance

The chain can extend to any depth:

class Being {
exist() { return "I exist"; }
}

class Animal extends Being {
eat() { return "I eat"; }
}

class Dog extends Animal {
bark() { return "I bark"; }
}

class Puppy extends Dog {
play() { return "I play"; }
}

const pup = new Puppy();

console.log(pup.play()); // "I play" (Puppy.prototype)
console.log(pup.bark()); // "I bark" (Dog.prototype)
console.log(pup.eat()); // "I eat" (Animal.prototype)
console.log(pup.exist()); // "I exist" (Being.prototype)

// Four levels of prototype chain (plus Object.prototype)
console.log(pup instanceof Puppy); // true
console.log(pup instanceof Dog); // true
console.log(pup instanceof Animal); // true
console.log(pup instanceof Being); // true
console.log(pup instanceof Object); // true
tip

While JavaScript supports any depth of inheritance, deep hierarchies (more than 2-3 levels) often become hard to maintain and understand. Prefer composition over inheritance when the hierarchy starts getting complex. Use inheritance for clear "is-a" relationships (Dog is an Animal) and composition for "has-a" relationships (Car has an Engine).

Putting It All Together: A Complete Example

class EventEmitter {
#listeners = {};

on(event, callback) {
if (!this.#listeners[event]) {
this.#listeners[event] = [];
}
this.#listeners[event].push(callback);
return this;
}

emit(event, ...args) {
const handlers = this.#listeners[event] || [];
handlers.forEach(handler => handler(...args));
return this;
}

static create() {
return new this();
}
}

class Store extends EventEmitter {
#state;

constructor(initialState = {}) {
super(); // Must call before using "this"
this.#state = initialState;
}

getState() {
return { ...this.#state };
}

setState(updates) {
const prevState = this.#state;
this.#state = { ...this.#state, ...updates };
// Emit a "change" event (inherited from EventEmitter)
this.emit("change", this.#state, prevState);
}

static createWithDefaults(defaults) {
const store = new this(defaults);
return store;
}
}

class UserStore extends Store {
constructor() {
super({ users: [], loading: false });
}

addUser(user) {
const { users } = this.getState();
this.setState({ users: [...users, user] });
}

removeUser(id) {
const { users } = this.getState();
this.setState({ users: users.filter(u => u.id !== id) });
}
}

// Usage
const store = new UserStore();

store.on("change", (newState, oldState) => {
console.log(`Users count changed: ${oldState.users.length}${newState.users.length}`);
});

store.addUser({ id: 1, name: "Alice" });
// "Users count changed: 0 → 1"

store.addUser({ id: 2, name: "Bob" });
// "Users count changed: 1 → 2"

store.removeUser(1);
// "Users count changed: 2 → 1"

// Instance checks work through the whole chain
console.log(store instanceof UserStore); // true
console.log(store instanceof Store); // true
console.log(store instanceof EventEmitter); // true

Summary

ConceptKey Takeaway
extendsEstablishes inheritance; sets up prototype chain for both instances and statics
Method overridingChild defines a method with the same name; child's version is used
super.method()Calls the parent class's version of a method from within the child
super() in constructorRequired in child constructors before using this; calls parent constructor
Default constructorIf omitted in a child, JS generates constructor(...args) { super(...args); }
[[HomeObject]]Internal property linking a method to its definition class; enables super resolution
Static inheritanceStatic methods and properties are inherited via the constructor prototype chain
Two chainsInstances: instance → Child.prototype → Parent.prototype → Object.prototype; Statics: Child → Parent → Function.prototype
instanceofWalks the instance prototype chain; instance instanceof Parent is true for all ancestors

Class inheritance in JavaScript provides a clean, standardized way to build hierarchies of related objects. The extends and super keywords handle the complex prototype chain setup that previously required manual configuration. Understanding both the surface syntax and the underlying prototype mechanism gives you the confidence to use inheritance effectively and debug it when things go wrong.