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:
- The object itself.
- Its prototype.
- That prototype's prototype.
- …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
- Confirm with
Object.hasOwnthat data is on the instance and methods are not. Then confirm two instances share the same method. - Find a method on
Order.prototypedirectly and call it with.call()from module 3. - Print
typeofa class and explain the answer. - Write
Subscription extends Orderwith its owndays, atotalthat multiplies, and adescribethat callssuper.describe(). - Remove the
super()call and read the error. Say in your own words whythiscannot exist yet. - 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. - Use
instanceofagainst the subclass, the parent, and something unrelated. - Add a method to
Order.prototypeafter creating an instance, then call it on that existing instance. It works — explain why, using the chain. - Rewrite
Subscriptionwith composition instead ofextends. Compare the two and decide which you would rather change in six months. - Harder. Build a three-level hierarchy —
Order,Subscription,GiftSubscription— where each level overridestotal. 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