RizTech Academy logo
RizTech Academy
Classes and ErrorsLesson 2 of 330 min

extends, super, and what a prototype really is

JavaScript had no classes until 2015, and it still does not have them in the way Java does. The class keyword is a clearer way to write something that already existed: prototypes. Knowing what is underneath explains several things that otherwise look arbitrary.

Where methods actually live

class Order {
  constructor(customer) {
    this.customer = customer;
  }

  describe() {
    return this.customer;
  }
}

const a = new Order('Priya');
const b = new Order('Arjun');

console.log(Object.hasOwn(a, 'customer'));
console.log(Object.hasOwn(a, 'describe'));
console.log(a.describe === b.describe);
true
false
true

Read those three carefully.

customer belongs to the instance — each order has its own. describe does not belong to the instance at all, and yet a.describe() works. And both orders have the same function, not two copies.

The method lives on Order.prototype, one object shared by every instance:

console.log(a.describe === Order.prototype.describe);
console.log(Object.getPrototypeOf(a) === Order.prototype);
true
true

The prototype chain

When you read a property, JavaScript looks in this order:

  1. The object itself.
  2. Its prototype.
  3. That prototype's prototype.
  4. …until null.

For a.describe(): not on a, found on Order.prototype, done. For a.toString(): not on a, not on Order.prototype, found on Object.prototype.

This is inheritance, and it is a chain of live objects rather than a compile- time copy. It is also the reason module 4 said for...in walks inherited properties and Object.keys does not, and why 'toString' in {} is true.

Two practical consequences:

Memory. One thousand orders share one describe function. A class field holding an arrow function — the third this fix from the last lesson — creates one function per instance instead. For a thousand objects that is a thousand functions. Usually irrelevant, occasionally not, and now you know which is which.

typeof a class is 'function':

console.log(typeof Order);
function

Because that is what it has always been. class is syntax over a function with a prototype object hanging off it.

extends

class Order {
  constructor(customer, plates) {
    this.customer = customer;
    this.plates = plates;
  }

  get total() {
    return this.plates * 80;
  }

  describe() {
    return `${this.customer}: ${this.plates} plates, ₹${this.total}`;
  }
}

class Subscription extends Order {
  constructor(customer, plates, days) {
    super(customer, plates);
    this.days = days;
  }

  get total() {
    return super.total * this.days;
  }

  describe() {
    return `${super.describe()} for ${this.days} days`;
  }
}

const s = new Subscription('Meera', 2, 5);

console.log(s.describe());
Meera: 2 plates, ₹800 for 5 days

extends puts Subscription.prototype in front of Order.prototype in the chain. A lookup checks the subclass first and falls back to the parent.

super means "the parent's version": super.total is the parent getter, super.describe() the parent method.

super() is compulsory

class Bad extends Order {
  constructor() {
    this.x = 1;
  }
}

new Bad();
ReferenceError: Must call super constructor in derived class before accessing 'this'

In a subclass constructor, this does not exist until super() has run, because the parent constructor is what creates it. super() first, then your own fields. Omit the constructor entirely and one calling super with all arguments is supplied for you.

The subtlety worth stopping for

Look again at that output: ₹800.

super.describe() is the parent's method, and it contains this.total. You might expect the parent's total — 2 plates at ₹80, so ₹160. It printed ₹800.

super.describe() runs the parent's code, but this is still the subscription. So this.total finds the subclass getter, which multiplies by five days. The method came from the parent; the property lookup did not.

That is polymorphism working correctly, and it surprises people every time. When a parent method calls another method, the child's override is what runs.

instanceof

console.log(s instanceof Subscription);
console.log(s instanceof Order);
console.log(s instanceof Array);
true
true
false

instanceof walks the prototype chain looking for that class's prototype. It is the right tool for "is this one of mine", and the next lesson uses it constantly to tell error types apart.

Prefer composition

Inheritance is taught first everywhere and is the wrong default.

The cost is coupling. A subclass depends on its parent's internals; change the parent and every subclass can break, including ones you have forgotten. And hierarchies do not survive contact with real requirements — a Subscription is a kind of Order until you need one that is paused, or one that is a gift, and suddenly you want two parents at once. JavaScript will not give you that.

Composition means holding a thing rather than being one:

class Subscription {
  constructor(order, days) {
    this.order = order;
    this.days = days;
  }

  get total() {
    return this.order.total * this.days;
  }
}

No inheritance. Subscription has an order. Swap it for a different kind of order and nothing breaks; there is no chain to reason about.

Use inheritance when the child genuinely is a kind of the parent and the parent is stable. The clearest case in everyday JavaScript is extending Error, which is the next lesson, and where the parent has not changed in twenty years.

Otherwise, hold the thing.

Check your work

Object.hasOwn(a, 'customer') is true and Object.hasOwn(a, 'describe') is false. Data lives on the instance, methods on the prototype.

a.describe === b.describe is true. One shared function for all instances — which is why a class field holding an arrow costs one function per object.

Object.getPrototypeOf(a) === Order.prototype is true.

typeof Order is 'function'. A class is a function with a prototype attached.

A subclass constructor that touches this before super() throws ReferenceError: Must call super constructor in derived class before accessing 'this'. The parent constructor creates the object.

s.describe() gives Meera: 2 plates, ₹800 for 5 days, not ₹160. super.describe() runs the parent's code with this still pointing at the subscription, so this.total resolves to the subclass getter. A parent method calling another method gets the child's override.

s instanceof Subscription and s instanceof Order are both true.

Adding to the prototype after an instance exists still affects that instance.

const inst = new Order('Priya');
Order.prototype.shout = function () {
  return this.customer.toUpperCase();
};

console.log(inst.shout());
PRIYA

The instance never held the method; it looks it up in a live object every time, so anything added later is found. This is the prototype chain's real nature, and also why modifying built-in prototypes is considered antisocial — you are editing an object every other script on the page is also reading.

Composition is the better default. Inheritance couples a subclass to its parent's internals and only allows one parent; holding an object does neither. The three-level exercise makes this concrete: a gift subscription that is also paused needs two parents, extends gives you one, and the only ways out are duplicating a class or holding the behaviours as fields.

Practice

  1. Confirm with Object.hasOwn that data is on the instance and methods are not. Then confirm two instances share the same method.
  2. Find a method on Order.prototype directly and call it with .call() from module 3.
  3. Print typeof a class and explain the answer.
  4. Write Subscription extends Order with its own days, a total that multiplies, and a describe that calls super.describe().
  5. Remove the super() call and read the error. Say in your own words why this cannot exist yet.
  6. Predict the total before running it. With 2 plates and 5 days, does super.describe() print ₹160 or ₹800? Run it and account for the answer.
  7. Use instanceof against the subclass, the parent, and something unrelated.
  8. Add a method to Order.prototype after creating an instance, then call it on that existing instance. It works — explain why, using the chain.
  9. Rewrite Subscription with composition instead of extends. Compare the two and decide which you would rather change in six months.
  10. Harder. Build a three-level hierarchy — Order, Subscription, GiftSubscription — where each level overrides total. Then add a requirement: a gift subscription that is also paused. Try to model it with inheritance, notice what goes wrong, and solve it with composition instead. That failure is the entire argument of this lesson.

Next: errors — throw, try/catch, and writing your own Error types, which is the one place inheritance is unambiguously right.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship