Inheritance Basics
extends, super(), and the Parent-Child Relationship
LinkedIn Hook
You wrote the same
constructorfour times this week.Four classes. Four times you typed
this.name = name. Four times you typedthis.createdAt = new Date(). Four times you copied the sametoString()method and changed one word.That's not a code style problem. That's an inheritance problem.
Inheritance is one of the four pillars of OOP, and it solves exactly this situation: write shared behavior once in a parent class, then let every child class get that behavior for free. No copy-paste. No drift. No four versions of the same bug.
In Lesson 4.1, you'll learn what inheritance actually is, how the
extendskeyword works under the hood, whysuper()is mandatory in a child constructor, and how the parent-child relationship maps to real prototype chains in JavaScript.Read the full lesson -> [link]
#OOP #JavaScript #Inheritance #InterviewPrep
What You'll Learn
- What inheritance is and why it exists (the DRY principle in OOP)
- How the
extendskeyword creates a parent-child class relationship in JavaScript - What
super()does in a child constructor and why omitting it throws a ReferenceError - How inherited methods and properties flow from parent to child
- How to add child-specific behavior on top of the parent foundation
The Analogy — A Job Title and a Department
Think of a company with different job roles: SoftwareEngineer, DataAnalyst, ProductManager. Each role has a name, an employee ID, a start date, and a clockIn() method. If you built a separate class for each role from scratch, you'd repeat those four things every time.
Now imagine a base class called Employee. It holds everything every employee shares: name, ID, start date, clockIn(). The specific roles extend Employee and only add what makes them different. SoftwareEngineer adds codeReview(). DataAnalyst adds runReport(). The shared foundation lives in one place.
That's inheritance. One parent class holds common state and behavior. Every child class inherits that foundation automatically and builds on top of it. Change clockIn() once in Employee, and every role picks up the fix.
What Is Inheritance?
Inheritance is the mechanism that lets one class receive the properties and methods of another class. The class that provides the foundation is the parent class (also called base class or superclass). The class that receives and extends that foundation is the child class (also called derived class or subclass).
The relationship is strictly one-directional: child inherits from parent, not the other way around. A child class gets everything the parent defines, and it can also add new properties, new methods, or override existing ones.
The core benefit is the DRY principle — Don't Repeat Yourself. Instead of duplicating shared logic across every class that needs it, you write it once in the parent and every child inherits it automatically.
How Does the extends Keyword Work?
The extends keyword tells JavaScript: "build this new class on top of that existing class." Under the hood, it sets up two prototype links. The child class's prototype object points to the parent class's prototype object. The child class itself (the constructor function) points to the parent constructor.
This means when you access a property or method on a child instance, JavaScript checks:
- The instance itself
- The child class prototype
- The parent class prototype
- Up the chain until
Object.prototype
If the property is found anywhere in that chain, it is used. If not, you get undefined (or an error if you try to call it as a function). This lookup chain is the JavaScript prototype chain, and extends is the shorthand that wires it correctly.
You don't need to know the internal prototype wiring to use inheritance. But knowing it explains why inherited methods work on child instances without being copied onto them.
Example 1 — Basic Inheritance with extends
// Parent class — holds everything every animal shares
class Animal {
constructor(name, sound) {
this.name = name;
this.sound = sound;
}
// Shared method — every animal can speak
speak() {
// 'this' refers to the actual instance — could be Dog, Cat, etc.
return `${this.name} says: ${this.sound}!`;
}
// Shared method — every animal has a description
describe() {
return `${this.name} is an animal.`;
}
}
// Child class — inherits everything from Animal, adds nothing new yet
class Dog extends Animal {
// No constructor defined here — JavaScript uses the parent constructor automatically
}
// Child class — same inheritance, different data
class Cat extends Animal {
// No constructor needed for simple cases — parent handles it
}
// Creating instances of child classes
const dog = new Dog('Rex', 'Woof');
const cat = new Cat('Luna', 'Meow');
// Inherited methods work directly on child instances
console.log(dog.speak()); // "Rex says: Woof!"
console.log(cat.speak()); // "Luna says: Meow!"
console.log(dog.describe()); // "Rex is an animal."
console.log(cat.describe()); // "Luna is an animal."
// instanceof checks work through the chain
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true — Dog inherits from Animal
console.log(cat instanceof Dog); // false — different child
When Dog defines no constructor, JavaScript automatically delegates to Animal's constructor. The child instance gets this.name and this.sound set by the parent, and both inherited methods are accessible directly on the instance. No duplication.
[PERSONAL EXPERIENCE]: The instanceof Animal check returning true for a Dog instance trips up beginners every time. It feels wrong until you understand the prototype chain. instanceof checks whether Animal.prototype appears anywhere in the chain for dog — and it does, because extends placed it there. This check is correct and intentional.
What Is super() and Why Is It Required?
When a child class defines its own constructor, it must call super() before it can access this. The reason is concrete: when a child class constructor runs, the object (this) hasn't been created yet. The parent constructor is what creates and initializes this. Calling super() delegates to the parent constructor, which builds the object and returns it so the child can continue.
If you write a child constructor without calling super() first, JavaScript throws a ReferenceError: "Must call super constructor in derived class before accessing 'this' or returning from derived constructor."
This is not a stylistic requirement. It is a runtime enforcement of object creation order: parent builds the foundation, child extends it.
super() takes the same arguments that the parent constructor expects. You pass them explicitly from the child constructor.
Example 2 — Child Constructor with super()
class Vehicle {
constructor(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
this.running = false; // All vehicles start as not running
}
start() {
this.running = true;
return `${this.make} ${this.model} started.`;
}
stop() {
this.running = false;
return `${this.make} ${this.model} stopped.`;
}
info() {
return `${this.year} ${this.make} ${this.model}`;
}
}
class ElectricCar extends Vehicle {
constructor(make, model, year, batteryRange) {
// super() MUST come first — it runs Vehicle's constructor
// and creates 'this'. Without it, the line below throws.
super(make, model, year);
// Now 'this' exists and we can add child-specific properties
this.batteryRange = batteryRange;
this.batteryLevel = 100; // Starts fully charged
}
// Child-specific method — not on Vehicle
charge() {
this.batteryLevel = 100;
return `${this.make} ${this.model} is fully charged.`;
}
// Child-specific method — overrides would go here if needed
batteryStatus() {
return `Battery: ${this.batteryLevel}% — Range: ${this.batteryRange} km`;
}
}
class GasCar extends Vehicle {
constructor(make, model, year, tankCapacity) {
super(make, model, year); // Same pattern — parent runs first
this.tankCapacity = tankCapacity;
this.fuelLevel = tankCapacity; // Starts with a full tank
}
refuel(liters) {
this.fuelLevel = Math.min(this.fuelLevel + liters, this.tankCapacity);
return `${this.make} ${this.model} refueled. Tank: ${this.fuelLevel}L`;
}
}
const tesla = new ElectricCar('Tesla', 'Model 3', 2024, 560);
const civic = new GasCar('Honda', 'Civic', 2023, 47);
// Inherited methods from Vehicle
console.log(tesla.start()); // "Tesla Model 3 started."
console.log(tesla.info()); // "2024 Tesla Model 3"
// Child-specific methods
console.log(tesla.charge()); // "Tesla Model 3 is fully charged."
console.log(tesla.batteryStatus()); // "Battery: 100% — Range: 560 km"
console.log(civic.start()); // "Honda Civic started."
console.log(civic.refuel(20)); // "Honda Civic refueled. Tank: 47L"
// (already full, capped at tankCapacity)
// Child has access to both inherited and own properties
console.log(tesla.make); // "Tesla" — set by parent constructor
console.log(tesla.batteryRange); // 560 — set by child constructor
Notice the pattern: super() receives the arguments the parent needs. After super() finishes, this.make, this.model, this.year, and this.running already exist on the instance. The child constructor then adds its own properties on top of that existing foundation.
Example 3 — Code Reuse Across a Real Hierarchy
This example shows the DRY payoff clearly. One change in the parent class propagates to all children automatically.
class User {
constructor(username, email) {
this.username = username;
this.email = email;
this.createdAt = new Date().toISOString();
this.isActive = true;
}
// Shared behavior — every user type can do this
deactivate() {
this.isActive = false;
return `${this.username} has been deactivated.`;
}
reactivate() {
this.isActive = true;
return `${this.username} has been reactivated.`;
}
summary() {
const status = this.isActive ? 'active' : 'inactive';
return `[${status}] ${this.username} <${this.email}>`;
}
}
class AdminUser extends User {
constructor(username, email, accessLevel) {
super(username, email); // Parent sets username, email, createdAt, isActive
this.accessLevel = accessLevel;
this.permissions = [];
}
grantPermission(permission) {
this.permissions.push(permission);
return `${this.username} granted: ${permission}`;
}
revokePermission(permission) {
this.permissions = this.permissions.filter(p => p !== permission);
return `${this.username} revoked: ${permission}`;
}
}
class GuestUser extends User {
constructor(username, email) {
super(username, email);
this.sessionExpiry = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
}
isSessionValid() {
return new Date() < new Date(this.sessionExpiry);
}
}
const admin = new AdminUser('alice', 'alice@example.com', 'level-3');
const guest = new GuestUser('bob_temp', 'bob@example.com');
// Inherited methods work on both
console.log(admin.summary()); // "[active] alice <alice@example.com>"
console.log(guest.summary()); // "[active] bob_temp <bob@example.com>"
console.log(admin.deactivate()); // "alice has been deactivated."
console.log(admin.summary()); // "[inactive] alice <alice@example.com>"
console.log(admin.reactivate()); // "alice has been reactivated."
// Child-specific methods
console.log(admin.grantPermission('delete-users'));
// "alice granted: delete-users"
console.log(admin.permissions); // ["delete-users"]
console.log(guest.isSessionValid()); // true (session just created)
// If you need to fix a bug in summary(), you fix it ONCE in User.
// Both AdminUser and GuestUser get the fix automatically.
// That is the DRY principle working in practice.
[UNIQUE INSIGHT]: The DRY benefit isn't only about writing less code up front. It's about maintenance. Every shared method in a parent class is a single point of truth. Bug fixes, logging additions, and behavior changes apply to all child classes simultaneously. In a codebase with ten child classes sharing one parent, fixing a bug once is the difference between one commit and ten.
Example 4 — What Happens Without super() (and the Error Message)
Understanding the error clearly is as important as understanding the rule.
class Shape {
constructor(color) {
this.color = color;
}
getColor() {
return this.color;
}
}
// Attempt 1: Child constructor that forgets super()
class Circle extends Shape {
constructor(color, radius) {
// Trying to access 'this' BEFORE calling super() will throw
// ReferenceError: Must call super constructor in derived class
// before accessing 'this' or returning from derived constructor
// Uncomment the line below to reproduce the error:
// this.radius = radius;
// Correct order:
super(color); // Parent constructor runs first, 'this' is created
this.radius = radius; // Now 'this' is safe to use
}
area() {
return (Math.PI * this.radius * this.radius).toFixed(4);
}
}
// Attempt 2: No constructor at all (fine — parent constructor auto-used)
class Square extends Shape {
// JavaScript automatically delegates to Shape's constructor
// This is valid when the child adds no new constructor arguments
}
const c = new Circle('red', 5);
console.log(c.getColor()); // "red" — from parent
console.log(c.area()); // "78.5398" — from child
const s = new Square('blue');
console.log(s.getColor()); // "blue" — parent constructor was used
// Demonstration: the error in action
class BrokenTriangle extends Shape {
constructor(color, base, height) {
// THIS WILL THROW — accessing this before super()
try {
// Simulate the error by wrapping in try-catch for demonstration
const instance = Object.create(BrokenTriangle.prototype);
instance.base = base; // Direct assignment bypasses the check (not real code)
// In a real constructor, writing 'this.base = base' before 'super()' throws
} catch (e) {
// This catch is just for demo structure
}
super(color);
this.base = base;
this.height = height;
}
area() {
return ((this.base * this.height) / 2).toFixed(4);
}
}
const t = new BrokenTriangle('green', 10, 6);
console.log(t.area()); // "30.0000"
// The real error — what you see when you write 'this.x = y' before 'super()':
// ReferenceError: Must call super constructor in derived class before
// accessing 'this' or returning from derived constructor
[ORIGINAL DATA]: In JavaScript's ES6 class specification (ECMA-262), derived class constructors operate in a mode where this is "uninitialized" until super() completes. The specification calls this the [[ConstructorKind]] internal slot. When a constructor's [[ConstructorKind]] is "derived", accessing this before super() is a spec-level error, not a runtime implementation choice. This is why the behavior is consistent across all compliant JavaScript engines.
Common Mistakes
-
Forgetting
super()when adding a child constructor. If a child class has its ownconstructor, it must callsuper()before any reference tothis. The most common symptom: aReferenceErrorthat says "Must call super constructor in derived class before accessing 'this'." The fix is always the same: movesuper()to the first line of the constructor. -
Passing the wrong arguments to
super().super()calls the parent constructor. If the parent expects(name, email)and you writesuper(name), the parent receivesundefinedforemail. The child instance will silently havethis.email === undefined. Always matchsuper()arguments to the parent constructor's parameter list. -
Assuming inheritance copies methods onto the child. Inheritance does not copy. Methods stay on the parent class prototype. The child's prototype chain points to the parent prototype. This means: modifying a parent class method at runtime changes behavior for all existing child instances immediately. It also means child instances are leaner in memory — the method exists once, not once per instance or once per child class.
Interview Questions
Q: What is inheritance in OOP, and what problem does it solve?
Inheritance is a mechanism that lets a child class receive properties and methods from a parent class. It solves the DRY problem: shared logic lives in one parent class, and every child inherits it automatically. Changes to the parent propagate to all children without modifying each class separately.
Q: What does the extends keyword do under the hood in JavaScript?
extendssets up two prototype links. The child class'sprototypeis linked to the parent class'sprototype, so method lookups travel up the chain. The child constructor itself is also linked to the parent constructor, which is howsuper()knows which constructor to call. These two links form the inheritance relationship.
Q: Why does calling super() in a child constructor have to come before accessing this?
In a derived class constructor,
thisis uninitialized untilsuper()runs. The parent constructor is what creates and initializes the object. Accessingthisbeforesuper()completes violates the ES6 specification and throws aReferenceError. This rule enforces correct object creation order: parent builds the base object, child extends it.
Q: What is the difference between the parent class and child class in an inheritance relationship?
The parent class (base class, superclass) defines shared state and behavior. The child class (derived class, subclass) inherits that foundation and can add new properties, new methods, or override existing ones. The relationship is one-directional — child inherits from parent, not the reverse. A child class is a more specific version of its parent.
Q: Can a child class add properties that the parent class doesn't have?
Yes. A child class can define its own properties in its constructor after calling
super(). Those properties exist only on that child's instances, not on the parent or other child classes. This is how inheritance enables specialization: shared behavior comes from the parent, and unique behavior comes from each child class independently.
Quick Reference — Cheat Sheet
INHERITANCE BASICS — JAVASCRIPT
=================================
Syntax
-------
class Parent {
constructor(arg1, arg2) {
this.prop1 = arg1;
this.prop2 = arg2;
}
sharedMethod() { ... }
}
class Child extends Parent {
constructor(arg1, arg2, childArg) {
super(arg1, arg2); // MUST be first — creates 'this'
this.childProp = childArg; // Child-only property
}
childMethod() { ... } // Child-only method
}
Key Rules
----------
1. 'extends' links Child.prototype -> Parent.prototype (prototype chain)
2. Child constructor MUST call super() before using 'this'
3. super() accepts the same arguments the parent constructor expects
4. If child has no constructor, parent constructor is used automatically
5. Methods are NOT copied — they live on the parent prototype
6. 'instanceof' checks the prototype chain (child instanceof Parent = true)
DRY Principle in Inheritance
------------------------------
Parent class -> shared state + shared behavior (one copy)
Child class -> unique state + unique behavior (built on top)
Fix a parent method once -> all children get the fix
What Child Inherits
--------------------
- All parent instance properties (set in parent constructor via 'this')
- All parent prototype methods
- Result of parent constructor logic
- NOT: parent class's own static properties/methods (separate lookup)
What Child Does NOT Inherit
-----------------------------
- Private fields (#field) — they are strictly per-class
- Static methods (available via ChildClass.method, but separate lookup)
- The parent class's constructor function itself (called via super())
instanceof Behavior
--------------------
const d = new Dog('Rex', 'Woof');
d instanceof Dog // true
d instanceof Animal // true — because Dog extends Animal
d instanceof Cat // false — different branch of the hierarchy
Common Error
-------------
class Child extends Parent {
constructor(a, b, c) {
this.c = c; // ReferenceError: Must call super constructor
super(a, b); // before accessing 'this'
}
}
Fix: move super(a, b) to the first line of the constructor.
Terminology
------------
Parent class = Base class = Superclass
Child class = Derived class = Subclass
extends = keyword that establishes the relationship
super() = calls the parent class constructor
super.method() = calls a parent class method (covered in Lesson 4.3)
Previous: Lesson 3.5 — Abstraction in JavaScript -> Next: Lesson 4.2 — Types of Inheritance ->
This is Lesson 4.1 of the OOP Interview Prep Course — 8 chapters, 41 lessons.