Inheritance with extends & super
Learn how JavaScript class inheritance connects prototypes, why derived constructors must call super, how method overrides work, and why super follows a method's original home.
- 01Read an extends chainExplain how child classes inherit instance and static behavior.
- 02Write safe derived constructorsCall
super()before touchingthisand predict field order. - 03Override deliberatelyReplace methods or extend parent behavior with
super.method().
The idea: a child class starts from a parent class
The Class basics lesson introduces classes as a compact way to write a constructor, methods, getters, setters, and public fields. This lesson adds the next piece: one class can extend another.
If User already knows how to store a name and describe a regular account, Admin extends User can reuse that foundation and add only the admin-specific parts. That is inheritance: a child type delegates missing behavior to a parent type.
Imagine every family has a recipe book. The parent book says how to make soup, bread, and salad. The child book starts with all of those recipes available, then adds a spicy sauce and rewrites the salad. That is the feel of extends: start with parent behavior, add or replace only what changes.
- In real life: The parent recipe book
- In JavaScript:
class Userwith shared methods - In real life: The child's book starts with those recipes
- In JavaScript:
class Admin extends User - In real life: The child adds a new recipe
- In JavaScript: A new
ban()orsettings()method - In real life: The child rewrites one recipe
- In JavaScript: An overriding
describe()method
Where the analogy stops: A real book would copy pages. JavaScript does not copy methods; it links objects so lookup can continue to the parent.
This is the class-syntax version of ideas you already saw in F.prototype & constructors: manual inheritance with Object.create and calling a parent constructor like User.call(this, name). extends does that wiring for you, and super gives you a safe way to call the parent constructor or parent method.
extends creates two linked chains
STEP THROUGHThe Prototype chain lesson taught the lookup rule: if a property is not found on one object, JavaScript follows that object’s prototype link. Class inheritance uses that same delegation model. There is no magic bag of copied methods.
A class is also a value: the constructor function object itself. So extends makes two links:
Admin.prototype → User.prototypefor instance methods.Admin → Userfor static fields and methods.
extends creates two linked chains. Step through both before Static members gets its own lesson next.
script
class User { static category = "person"; greet() { return "hi"; }}class Admin extends User {}console.log(Object.getPrototypeOf(Admin) === User);console.log(new Admin().greet());console.log(Admin.category);| Goal | Manual pattern | Class pattern |
|---|---|---|
| Connect instance methods | Admin.prototype = Object.create(User.prototype) | class Admin extends User {} |
| Run parent setup | User.call(this, name) inside Admin | super(name) inside Admin |
| Fix constructor property | Reset Admin.prototype.constructor manually | Handled by class syntax |
| Inherit statics | Link the constructor functions manually | Object.getPrototypeOf(Admin) === User |
This is why inheritance is still delegation in JavaScript. The child asks its own prototype first. If the method is not there, lookup keeps walking to the parent prototype.
super() in constructors
INTERACTIVEA class that extends another class is called a derived class. In a derived constructor, this does not exist for you yet. The parent constructor must create and initialize the instance first, so the child must call super(...) before reading or writing this.
If you are building a house with your parent, you do not hang paintings before anyone has poured the foundation. A derived constructor has the same rule: let the parent set up the instance, then decorate it with child-specific fields.
- In real life: The parent prepares the foundation
- In JavaScript:
super(name)runs the parent constructor - In real life: You decorate after the foundation exists
- In JavaScript: Write
this.role = "admin"aftersuper() - In real life: Decorating empty air fails
- In JavaScript: Reading
thisbeforesuper()throws
Where the analogy stops: The analogy gives the reason, but the exact order of public fields is a JavaScript rule. The replay below proves the real order.
Step through new Admin("Ada"). Notice the subtle field order: the derived constructor starts, super() begins, base fields initialize, the base constructor body runs, derived fields initialize, and only then does the rest of the derived constructor continue.
Predict the first log from new Admin(...), then step through the constructor chain. Change the name to prove only the value changes, not the order.
script
const log = [];class User { role = log.push("base field: role"); constructor(name) { log.push("base constructor: " + name); this.name = name; }}class Admin extends User { level = log.push("derived field: level"); constructor(name) { super(name); log.push("derived constructor: after super"); }}new Admin("Ada");console.log(log.join(" → "));- A derived constructor can log a message before calling
super(). - Base public fields initialize.
- The parent constructor body assigns
this.name. - Derived public fields initialize.
- The derived constructor runs the line after
super(name). - Reading
this.namein a derived constructor beforesuper().
Sort each event by whether it happens before the super() call, inside that call, or after it returns. The tricky card throws if you actually try it.
In V8-based engines, touching this before super() throws a ReferenceError whose message is: “Must call super constructor in derived class before accessing 'this' or returning from derived constructor”. Other engines may phrase the same rule differently.
Overriding methods, with and without super.method()
STEP THROUGHAn override is a child method with the same name as a parent method. Lookup finds the child method first, so the child can replace behavior. Sometimes that is exactly what you want.
Other times, the child should keep the parent behavior and add one small piece. That is when super.method() is useful: ask the parent to do its version, then add yours.
If your parent already makes a good base soup, you can ask for that soup and add chili oil. You do not rewrite the whole recipe. But if the child dish is truly different, you can replace the recipe completely.
- In real life: Parent makes the basic soup
- In JavaScript:
super.settings()returns shared settings - In real life: Child adds chili oil
- In JavaScript: Child appends
danger zone - In real life: Child makes a totally different dish
- In JavaScript: Override without calling
super
Where the analogy stops: The parent is not a separate person at runtime. super.method() is a special lookup rule that still calls the method with the current this.
Step through two kinds of overriding: replacing a method completely, and extending the parent method with super.method().
script
class User { constructor(name) { this.name = name; } describe() { return this.name + " is a user"; } settings() { return ["profile", "notifications"]; }}class Admin extends User { describe() { return this.name + " is an admin"; } settings() { return [...super.settings(), "danger zone"]; }}console.log(ada.describe());console.log(ada.settings().join(", "));In the replay, describe() ignores the parent method, while settings() uses super.settings() to keep the parent settings and add one more. Both are legitimate; choose the one that matches your intent.
super and [[HomeObject]]
STEP THROUGHsuper looks simple, but it relies on a hidden spec detail. Methods written with method syntax receive an internal home address called [[HomeObject]]. When the method says super.greet(), JavaScript starts lookup above that home.
Suppose your official address is your parents’ house. You can visit a friend, but official mail still routes through the address on file. A method copied to another object is similar: the call receiver can change, but super still uses the method’s original home.
- In real life: A home address on official paperwork
- In JavaScript: The method’s
[[HomeObject]] - In real life: Visiting a friend’s house
- In JavaScript: Copying the method to another object
- In real life: Mail still goes to the official address
- In JavaScript:
superstill starts above the original home
Where the analogy stops: The method can still be called with a different this. The fixed part is where super starts lookup, not every value used by the method.
This is the surprising [[HomeObject]] rule: super is based on where a method was created, not where you later copy it.
script
const grand = { label: "grand", greet() { return this.name + " via grand"; }};const parent = { __proto__: grand, label: "parent",};const otherBase = { label: "other base", greet() { return this.name + " via other base"; }};const other = { __proto__: otherBase, name: "Copied method" };other.greet = parent.greet;console.log(other.greet());That surprising result is why super only works in method contexts that JavaScript can give a home. It is also why you should not casually copy methods that use super and expect them to adapt to a new prototype chain.
Rules and edge cases
Most inheritance code follows a small set of rules. Keep this table nearby until the patterns feel automatic.
| Where | Rule | Example |
|---|---|---|
| Derived constructor | Call super(...) before reading or writing this. | constructor(name) { super(name); this.role = "admin"; } |
| Method override | A child method with the same name is found before the parent method. | Admin.prototype.describe wins over User.prototype.describe. |
super.method() | Inside method syntax, call the parent version with the current this. | return super.settings().concat("danger zone") |
| Nested arrow | An arrow inside a method can use that method's super binding. | method() { return () => super.method(); } |
| Standalone arrow/function | Do not define overrides with super in a plain function or detached arrow. | Use class or object method syntax so JavaScript creates [[HomeObject]]. |
class User { constructor(name) { this.name = name; }}class Admin extends User { constructor(name) { this.name = name; super(name); }}new Admin("Ada");class User { constructor(name) { this.name = name; }}class Admin extends User { constructor(name) { super(name); this.role = "admin"; }}console.log(new Admin("Ada").role);The right side of extends is an expression. A pattern like class A extends mixin(B) is valid when mixin(B) evaluates to a class. The Mixins lesson explores that idea later. JavaScript also has extends null, a rare edge case for classes with no ordinary object prototype.
Where you’ll use this
In modern front-end code, inheritance is not the default tool for every reuse problem. Delegation vs classical inheritance already warned you about deep family trees. Still, extends is useful when the relationship is honest: the child really is a specialized version of the parent and should honor the same public methods.
- A design-system component class might extend a small base widget class that owns shared lifecycle hooks.
- A test helper can extend a fake service and override one method while reusing the rest.
- Framework and platform APIs often ask you to extend a base class. Extending built-ins gets its own lesson after private fields.
One parent and one child can be clear. Five levels of inheritance can hide cause and effect. If the child only needs a helper, pass an object in or call a function instead of using extends.
Common misconceptions
“extends copies the parent’s methods.”
It links prototypes. Missing properties are found by delegation, just like in the Prototype chain lesson.
“The parent constructor always runs before the child constructor.”
The child constructor is entered first, but it cannot use this until super() runs the parent construction.
“super is the same as this.”
this is the current receiver. super is a parent lookup based on the method’s [[HomeObject]].
“If I copy a method, super follows the new object.”
The copied-method experiment disproves that. super keeps using the method’s original home.
“Inheritance is always cleaner than composition.”
Inheritance is best for true is-a relationships. For “uses a” relationships, composition is usually easier to change.
Practice: inherit, override, and explain
5 EXERCISESRead the finished version, then type the printed output. In your own editor, try deleting super(name) to see why it is needed.
class User {
constructor(name) { this.name = name; }
label() { return this.name; }
}
class Admin extends User {
constructor(name, level) {
super(name);
this.level = level;
}
label() { return super.label() + " (level " + this.level + ")"; }
}
console.log(new Admin("Ada", 2).label());class User {
constructor(name) { this.name = name; }
label() { return this.name; }
}
class Admin extends User {
constructor(name, level) {
super(name);
this.level = level;
}
label() { return super.label() + " (level " + this.level + ")"; }
}
console.log(new Admin("Ada", 2).label());Admin extends User, so it can call the parent constructor with super(name). The override uses super.label() to reuse the parent label and append the level.
What is the first item in the log when new Admin("Ada") runs?
const log = [];
class User {
role = log.push("base field: role");
constructor(name) {
log.push("base constructor: " + name);
this.name = name;
}
}
class Admin extends User {
level = log.push("derived field: level");
constructor(name) {
log.push("derived constructor: before super");
super(name);
log.push("derived constructor: after super");
}
}
new Admin("Ada");
console.log(log.join(" → "));The first item is derived constructor: before super. After that, super() initializes the base field, runs the base constructor, returns to derived fields, and finally continues the derived constructor body.
The starter code touches this too early. Fix the order so the program prints the admin role.
class User {
constructor(name) { this.name = name; }
}
class Admin extends User {
constructor(name) {
this.name = name;
super(name);
}
}
new Admin("Ada");class User {
constructor(name) { this.name = name; }
}
class Admin extends User {
constructor(name) {
super(name);
this.role = "admin";
}
}
console.log(new Admin("Ada").role);The fixed constructor calls super(name) first, so this is available. Then it adds the child-specific role field and the program prints admin.
Keep the parent settings and add one admin-only setting. What does the final join print?
class User {
settings() { return ["profile", "notifications"]; }
}
class Admin extends User {
settings() { return [...super.settings(), "danger zone"]; }
}
console.log(new Admin().settings().join(", "));class User {
settings() { return ["profile", "notifications"]; }
}
class Admin extends User {
settings() { return [...super.settings(), "danger zone"]; }
}
console.log(new Admin().settings().join(", "));Admin.settings() calls the parent method with super.settings(), spreads those settings into a new array, then appends danger zone.
In one phrase, which base does super.greet() use after the method is copied to other?
const grand = {
label: "grand",
greet() { return this.name + " via grand"; }
};
const parent = {
__proto__: grand,
label: "parent",
greet() { return super.greet() + " → parent"; }
};
const otherBase = {
label: "other base",
greet() { return this.name + " via other base"; }
};
const other = { __proto__: otherBase, name: "Copied method" };
other.greet = parent.greet;
console.log(other.greet());The copied method uses the original parent chain. this is other, so the name comes from other, but super.greet() starts above parent, at grand, because the method’s [[HomeObject]] is still parent.
Quiz: check your understanding
7 QUESTIONSThese questions mix concepts with real output. Trace the lookup chain and constructor order before choosing.
Question 1 of 7What does
class Admin extends Userdo first conceptually?Choose an answer to see the explanation.
Question 2 of 7What does the overriding greet method print?
Read the code, then predictclass User { greet() { return "user"; } } class Admin extends User { greet() { return "admin"; } } console.log(new Admin().greet());Choose an answer to see the explanation.
Question 3 of 7Why must a derived constructor call
super()before usingthis?Choose an answer to see the explanation.
Question 4 of 7What does the super.settings override print?
Read the code, then predictclass User { settings() { return ["profile"]; } } class Admin extends User { settings() { return [...super.settings(), "admin"]; } } console.log(new Admin().settings().join(" + "));Choose an answer to see the explanation.
Question 5 of 7What is
[[HomeObject]]used for?Choose an answer to see the explanation.
Question 6 of 7What do the static chain checks print?
Read the code, then predictclass User { static kind = "person"; } class Admin extends User {} console.log(Object.getPrototypeOf(Admin) === User); console.log(Admin.kind);Choose an answer to see the explanation.
Question 7 of 7Which
extendsform is valid JavaScript?Choose an answer to see the explanation.
Key takeaways
extendslinks child and parent classes; it does not copy methods.- A derived constructor must call
super()before usingthis. - Base fields initialize inside
super(); derived fields initialize after it returns. - An override can replace a parent method or build on it with
super.method(). superin methods uses[[HomeObject]], so copied methods keep their original super lookup.
Remember the one-liner.
Class inheritance links a child class to a parent; super calls the parent constructor or method from that linked relationship.
Up next: Static members & static blocks.