Inheritance & Prototypes

JavaScript · Lesson 29 of 61 7 min read Updated

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.

JavaScriptRun it yourself
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);
Output
Farhan: ₹780
Isha: ₹1080
true true
false

Look at three details:

  • TatkalTicket never defines describe(), yet tatkal.describe() works. The method was inherited from Ticket.
  • Inside describe(), the call this.price() uses the child's version for tatkal. 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.
  • instanceof checks whether an object was made by a class or any of its children. A Tatkal ticket is a Ticket too, but a normal ticket is not a TatkalTicket.

The two jobs of super

The word super does two different jobs:

CodeWhereWhat it does
super(a, b)In the child's constructorRuns the parent's constructor
super.method()In any child methodCalls 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.

JavaScript
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:

JavaScriptRun it yourself
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);
Output
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.

JavaScriptRun it yourself
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);
Output
true
true
true
false 500
function

So when you call ticket.price(), the search goes like this:

Text
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.

JavaScript
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 this before super(). In a child constructor, call super(...) on the first line.
  • Forgetting to pass arguments to super(). super() with no arguments leaves the parent's properties undefined.
  • 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 Coach makes no sense. Put coach objects inside the train.

Key points

  • class Child extends Parent makes a child class that inherits every parent method.
  • A child constructor must call super(...) before it uses this; 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
JavaScript
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()); // 1000000

Manager 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?

2. Where does JavaScript look if a property is not found on the object itself?

3. Object.hasOwn(car, "wheels") is false but "wheels" in car is true. Why?

4. A school timetable belongs to a student. Which design fits best?

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