JavaScript inheritance is two keywords doing two separate jobs. extends wires a new class into an existing prototype chain, so instances of the child can find the parent's methods. super is how the child talks back to the parent: super(...) runs the parent's constructor, and super.method() runs a parent method against the current object. Get those two ideas straight and the rest of the topic is detail.
The example here is a small notification system: a generic Notification, an EmailNotification and an SmsNotification. It is deliberately ordinary, because inheritance earns its keep in exactly this kind of hierarchy and nowhere else.
What does extends actually do?
It creates the links. The child's prototype object gets the parent's prototype object as its own prototype, and the child constructor gets the parent constructor as its own prototype. That gives you two chains: a chain of objects for instances, and a chain of constructors for static members.
class Notification {
constructor(recipient) {
this.recipient = recipient;
this.sent = false;
}
send() {
this.sent = true;
return "notification sent to " + this.recipient;
}
}
class EmailNotification extends Notification {
constructor(recipient, subject) {
super(recipient);
this.subject = subject;
}
}
const mail = new EmailNotification("[email protected]", "Invoice");
console.log(mail.recipient);
console.log(mail.subject);
console.log(mail.sent);
console.log(mail instanceof EmailNotification);
console.log(mail instanceof Notification);
The recipient and the subject both land on the object, and sent starts false. Both instanceof checks print true, which is the practical payoff: an email notification really is a notification, and code that accepts "any notification" can accept one without caring which kind it is.
Here is the chain underneath, with nothing hidden.
class Notification {}
class EmailNotification extends Notification {}
const mail = new EmailNotification();
console.log(Object.getPrototypeOf(mail) === EmailNotification.prototype);
console.log(Object.getPrototypeOf(EmailNotification.prototype) === Notification.prototype);
console.log(Object.getPrototypeOf(EmailNotification) === Notification);
console.log(mail instanceof Notification);
console.log(mail instanceof EmailNotification);
Read the instance chain first: the mail object, then EmailNotification.prototype, then Notification.prototype, then Object.prototype. A method that only exists on Notification.prototype is still reachable from an email notification, because the lookup walks up. The third line is the other chain: the child constructor's prototype is the parent constructor, which is what makes static members inheritable. The last two lines print true, and instanceof is quietly doing the same walk as the prototype lookup — it checks whether Notification.prototype appears anywhere in the object's chain.
When must you call super, and where?
In a derived class constructor, you must call super(...) exactly once, and you must do it before you touch this in any way. The reason is that this does not exist yet at the start of a derived constructor: the object is not created until the parent constructor has run, because the parent constructor is the code that decides what the object looks like.
class Notification {
constructor(recipient) {
this.recipient = recipient;
}
}
class BrokenNotification extends Notification {
constructor(recipient) {
try {
this.subject = "oops";
super(recipient);
} catch (err) {
console.log("first failure: " + err.constructor.name + ": " + err.message);
}
}
}
try {
new BrokenNotification("[email protected]");
} catch (err) {
console.log("second failure: " + err.constructor.name + ": " + err.message);
}
Both lines print the same ReferenceError, with the message Must call super constructor in derived class before accessing 'this' or returning from derived constructor. The first failure is the assignment to this.subject, which happens before super has run. The second is the constructor trying to finish without ever having called super. Two different mistakes, one message.
The practical rule: make super(...) the first statement in the constructor, then assign your own fields. If you find yourself wanting to compute something before super, compute it into a local variable first and pass it in — the constructor arguments are evaluated before the call, so they are allowed to use anything you like.
How do you call the parent's version of a method?
With super.method(). It runs the parent's implementation, but with this still pointing at the current object, so the parent method sees the child's data.
class Notification {
constructor(recipient) {
this.recipient = recipient;
this.sent = false;
}
describe() {
return "to " + this.recipient;
}
send() {
this.sent = true;
return "notification sent " + this.describe();
}
}
class EmailNotification extends Notification {
constructor(recipient, subject) {
super(recipient);
this.subject = subject;
}
describe() {
return "to " + this.recipient + " about " + this.subject;
}
send() {
const result = super.send();
return result + " via email";
}
}
const mail = new EmailNotification("[email protected]", "Invoice");
console.log(mail.send());
console.log(mail.sent);
console.log(mail.describe());
The first line prints notification sent to [email protected] about Invoice via email. Look closely at why the subject appears in the middle: super.send() runs the parent's method, and that method calls this.describe() — which resolves to the child's overridden version, not the parent's. The parent's method and the child's method are working together on the same object, and the child wins every override. mail.sent is true, because the parent's send set it, and the last line prints to [email protected] about Invoice.
Notice that super.send() is called as a method, so this inside it is the mail object. This is what makes overriding safe. If the parent method assigned to this.sent, it updated the child's object rather than some separate parent instance.
Why does inheritance make a mixed list of types easier?
Because you can write the loop once. Each subclass overrides the same method name, and the caller never needs a type check.
class Notification {
constructor(recipient) {
this.recipient = recipient;
}
send() {
return "base notification for " + this.recipient;
}
}
class EmailNotification extends Notification {
send() {
return "email to " + this.recipient;
}
}
class SmsNotification extends Notification {
send() {
return "sms to " + this.recipient;
}
}
const outbox = [
new Notification("[email protected]"),
new EmailNotification("[email protected]"),
new SmsNotification("+911234567890"),
];
for (const item of outbox) {
console.log(item.send());
}
The loop prints base notification for [email protected], then email to [email protected], then sms to +911234567890. Each object answers the same call with its own behaviour, and the loop does not care which is which. That is polymorphism, and it is the reason to reach for inheritance in the first place.
If you find yourself writing if (item instanceof EmailNotification) branches inside the loop, that is a signal that the hierarchy is not modelling the problem — often composition, where each object holds a list of the things it can do, is a better fit.
Do static members get inherited as well?
Yes. The chain of constructors is the chain of static members, so a child class sees the parent's static methods unless it defines its own.
class Notification {
static channel() {
return "generic";
}
static priority() {
return "normal";
}
}
class EmailNotification extends Notification {
static channel() {
return super.channel() + "/email";
}
}
console.log(EmailNotification.channel());
console.log(EmailNotification.priority());
console.log(Object.getPrototypeOf(EmailNotification) === Notification);
EmailNotification.channel() prints generic/email, because the override calls the parent's version through super. EmailNotification.priority() prints normal, because priority was never overridden and is found up the constructor chain. The last line prints true, which is the mechanism behind both: the child constructor's prototype is the parent constructor, and static methods live on constructors.
What are the traps worth knowing before you inherit anything?
supermust run beforethisin a derived constructor, and it may only run once. Calling it a second time throws aReferenceErrorreadingSuper constructor may only be called once.superonly exists inside the methods and constructors of a class. Using it in a plain function is a syntax error, not a runtime one.- An arrow function inside a method has no
superof its own, sosuperinside an arrow refers to the enclosing method's class. That is handy, but it means you cannot usesuperin a callback that is meant to run outside the class. - A parent constructor that returns an object changes what
thismeans in the child, becausethisis then that returned object. Rare, but surprising when it happens. - Inheritance couples the child to the parent's internals. If a parent's field is renamed, every child that touched it breaks. Composition keeps that coupling on one side.
Before you move on
extendslinks the child's prototype to the parent's prototype, and the child's constructor to the parent's constructor.super(...)runs the parent's constructor; it must come first and it may only be called once.super.method()runs a parent method withthisstill pointing at the child object, which is what makes overriding compose instead of replace.- Overrides resolve at call time, so the parent's method sees the child's version of any method it calls.
- Static members inherit through the constructor chain, exactly like prototype methods inherit through the prototype chain.
- Use inheritance for genuine is-a relationships. For has-a, hold a reference instead.
Try it yourself: build class Shape with a constructor taking a name and a method describe() that returns "a " + this.name. Then build class Circle extends Shape whose constructor calls super(name), stores a radius, and overrides describe() to call super.describe() and append " with radius " + this.radius. Create a circle and check that describe() prints both parts, that circle instanceof Shape is true, and that Object.getPrototypeOf(Circle.prototype) === Shape.prototype is true. Finally, write a deliberate bug — put this.radius = radius above the super(name) call — and run it. Read the ReferenceError carefully, then fix the order and re-run.