Inheritance & Prototypes
Often one kind of object is a special version of another. A Tatkal train ticket is still a ticket, but its price has an extra charge. With inheritance, a new class can reuse everything an existing class does and change only the parts that differ.
In this lesson you will use extends and super, override methods, and look under the hood at prototypes, the mechanism JavaScript uses to share methods between objects. Read Classes first if you have not already.
Extending a class
The keyword extends creates a child class (also called a subclass) from a parent class (a superclass). The child gets all of the parent's methods. It can add new ones and replace, or override, existing ones.
class Ticket {
constructor(passenger, baseFare) {
this.passenger = passenger;
this.baseFare = baseFare;
}
price() {
return this.baseFare;
}
describe() {
return `${this.passenger}: ₹${this.price()}`;
}
}
class TatkalTicket extends Ticket {
constructor(passenger, baseFare, surcharge) {
super(passenger, baseFare); // run the parent constructor first
this.surcharge = surcharge;
}
price() {
return super.price() + this.surcharge;
}
}
const normal = new Ticket("Farhan", 780);
const tatkal = new TatkalTicket("Isha", 780, 300);
console.log(normal.describe());
console.log(tatkal.describe());
console.log(tatkal instanceof TatkalTicket, tatkal instanceof Ticket);
console.log(normal instanceof TatkalTicket);Farhan: ₹780 Isha: ₹1080 true true false
Look at three details:
TatkalTicketnever definesdescribe(), yettatkal.describe()works. The method was inherited fromTicket.- Inside
describe(), the callthis.price()uses the child's version fortatkal. The parent's code automatically works with the child's override. This is called polymorphism: the same call does the right thing for each kind of object. instanceofchecks whether an object was made by a class or any of its children. A Tatkal ticket is aTickettoo, but a normal ticket is not aTatkalTicket.
The two jobs of super
The word super does two different jobs:
| Code | Where | What it does |
|---|---|---|
super(a, b) | In the child's constructor | Runs the parent's constructor |
super.method() | In any child method | Calls the parent's version of a method |
If a child class has a constructor, it must call super() before it uses this. The parent sets up the object first, then the child adds its own parts. Break this rule and JavaScript throws a ReferenceError the moment the object is created.
class TatkalTicket extends Ticket {
constructor(passenger, baseFare, surcharge) {
this.surcharge = surcharge; // ReferenceError: super() has not run yet
super(passenger, baseFare);
}
}If the child does not need any extra setup, leave the constructor out completely. JavaScript then gives it a default constructor that passes every argument to the parent.
super.method() is useful when you want to extend the parent's behaviour rather than replace it. In TatkalTicket, price() takes the parent's price and adds the surcharge. If the base fare rule ever changes in Ticket, the Tatkal price follows automatically.
What are prototypes?
Classes arrived in JavaScript in 2015, but inheritance existed long before that. Underneath, it works through prototypes.
Every object has a hidden link to another object, called its prototype. When you read a property, JavaScript first looks on the object itself. If the property is not there, it follows the link and looks on the prototype, then on the prototype's prototype, and so on. This chain of links is the prototype chain. The search stops at null.
You can see this with plain objects. Object.create(proto) makes a new object whose prototype is proto:
const vehicle = {
wheels: 4,
describe() {
return `${this.name} has ${this.wheels} wheels`;
},
};
const auto = Object.create(vehicle); // the prototype of auto is vehicle
auto.name = "Auto-rickshaw";
auto.wheels = 3;
const car = Object.create(vehicle);
car.name = "Swift";
console.log(auto.describe());
console.log(car.describe());
console.log(Object.getPrototypeOf(car) === vehicle);
console.log(Object.hasOwn(car, "wheels"), "wheels" in car);Auto-rickshaw has 3 wheels Swift has 4 wheels true false true
Neither auto nor car has its own describe method, so JavaScript finds it on vehicle. The auto has its own wheels property, which shadows (hides) the one on the prototype. The car has no wheels of its own, so it reads 4 from vehicle. Object.hasOwn() checks only the object itself, while the in operator searches the whole chain.
Classes are built on prototypes
A class is a neat way to set up prototype chains. Methods you write in a class body are stored on ClassName.prototype, and every instance links to that object. With extends, the child's prototype object links to the parent's prototype object.
class Ticket {
price() {
return 500;
}
}
class TatkalTicket extends Ticket {}
const ticket = new TatkalTicket();
console.log(Object.getPrototypeOf(ticket) === TatkalTicket.prototype);
console.log(Object.getPrototypeOf(TatkalTicket.prototype) === Ticket.prototype);
console.log(Object.getPrototypeOf(Ticket.prototype) === Object.prototype);
console.log(Object.hasOwn(ticket, "price"), ticket.price());
console.log(typeof ticket.toString);true true true false 500 function
So when you call ticket.price(), the search goes like this:
ticket -> TatkalTicket.prototype -> Ticket.prototype -> Object.prototype -> null
(own data) (nothing here) (price found here) (toString, hasOwnProperty...)That is also why every object has a toString() method: it lives on Object.prototype, at the end of almost every chain.
Inheritance or composition?
Inheritance describes an is-a relationship: a Tatkal ticket is a ticket. Many real relationships are has-a instead: a train has coaches, a student has a timetable. For those, store one object inside another. This is called composition.
class Coach {
constructor(code, seats) {
this.code = code;
this.seats = seats;
}
}
class Train {
constructor(name, coaches) {
this.name = name;
this.coaches = coaches; // a Train has Coach objects
}
totalSeats() {
return this.coaches.reduce((sum, coach) => sum + coach.seats, 0);
}
}You can also extend built-in classes. The most useful case is creating your own error types with class ValidationError extends Error, which you will do in Errors & Exceptions.
Common mistakes
- Using
thisbeforesuper(). In a child constructor, callsuper(...)on the first line. - Forgetting to pass arguments to
super().super()with no arguments leaves the parent's propertiesundefined. - Copying a parent method instead of calling it. If the child only adds to the parent's result, call
super.method()rather than pasting the parent's code. - Using inheritance for has-a relationships.
class Train extends Coachmakes no sense. Put coach objects inside the train.
Key points
class Child extends Parentmakes a child class that inherits every parent method.- A child constructor must call
super(...)before it usesthis;super.method()calls the parent's version. - Overriding a method in the child changes behaviour even inside inherited parent methods.
- Every object links to a prototype; property lookups follow the prototype chain until
null. - Classes store methods on
ClassName.prototype, so classes are a clean syntax over prototypes. - Prefer composition for has-a relationships and keep inheritance chains shallow.
Exercise
Create a class Employee with name and salary and a method yearlyPay() that returns salary * 12. Then create Manager extends Employee with an extra bonus property, and override yearlyPay() so it adds the bonus by calling the parent's version. Create one employee and one manager and log both yearly amounts.
Show solution
class Employee {
constructor(name, salary) {
this.name = name;
this.salary = salary;
}
yearlyPay() {
return this.salary * 12;
}
}
class Manager extends Employee {
constructor(name, salary, bonus) {
super(name, salary);
this.bonus = bonus;
}
yearlyPay() {
return super.yearlyPay() + this.bonus;
}
}
const rohan = new Employee("Rohan", 40000);
const ananya = new Manager("Ananya", 75000, 100000);
console.log(rohan.yearlyPay()); // 480000
console.log(ananya.yearlyPay()); // 1000000Manager reuses the parent's calculation with super.yearlyPay() and only adds what is different, the bonus.
Quick Quiz
1. In a child class constructor, what must happen before you use this?
The parent constructor sets up the object, so super(...) must run before the child touches this.
2. Where does JavaScript look if a property is not found on the object itself?
Property lookup follows the prototype chain until it finds the property or reaches null.
3. Object.hasOwn(car, "wheels") is false but "wheels" in car is true. Why?
Object.hasOwn checks only the object's own properties; in searches the whole prototype chain.
4. A school timetable belongs to a student. Which design fits best?
This is a has-a relationship, so composition fits better than inheritance.
Learn JavaScript with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
