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:
Dog.prototype.__proto__is set toAnimal.prototype(so instances ofDogcan accessAnimal's methods)Dog.__proto__is set toAnimal(soDoginherits static methods fromAnimal)
// 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:
new Dog("Rex", "Shepherd")is called- JavaScript sees
Dog extends Animal, so it does NOT create an object yet Dog's constructor runs, and it MUST callsuper(name)super(name)callsAnimal's constructor, which creates the object and setsthis.name- Control returns to
Dog's constructor, wherethisis now the created object Dog's constructor can now setthis.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}`;
}
}
}
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:
- Looks at the
[[HomeObject]]of the current method - Goes to
[[HomeObject]].__proto__(the parent's prototype) - Finds
methodthere 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
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:
- Looks for
createonDogitself: not found - Follows
Dog.__proto__toAnimal: found! UsesAnimal.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
Verifying Every Link
// 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():
- JavaScript checks
rex(the instance) for aneatproperty: not found - Follows
rex.[[Prototype]]toDog.prototype: noeatthere - Follows
Dog.prototype.[[Prototype]]toAnimal.prototype:eatfound! - Calls
eatwiththis = rex
When you call Dog.kingdom:
- JavaScript checks
Dogfor akingdomproperty: not found - Follows
Dog.[[Prototype]]toAnimal:kingdomfound! - 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
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
| Concept | Key Takeaway |
|---|---|
extends | Establishes inheritance; sets up prototype chain for both instances and statics |
| Method overriding | Child 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 constructor | Required in child constructors before using this; calls parent constructor |
| Default constructor | If omitted in a child, JS generates constructor(...args) { super(...args); } |
[[HomeObject]] | Internal property linking a method to its definition class; enables super resolution |
| Static inheritance | Static methods and properties are inherited via the constructor prototype chain |
| Two chains | Instances: instance → Child.prototype → Parent.prototype → Object.prototype; Statics: Child → Parent → Function.prototype |
instanceof | Walks 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.