Inheritance with extends & super Interview Questions & Answers
24 questions Updated 2026-06-18
JavaScript class inheritance interview questions — extends, super, overriding, constructor chaining, polymorphism and extending built-ins.
Read the in-depth guideJavaScript Class Inheritance — extends, super, and Method Overriding Explainedclass Child extends Parent makes Child inherit from Parent: instances get
Parent's methods via the prototype chain, and Child itself inherits
Parent's statics.
class Animal {
constructor(name) { this.name = name }
speak() { return `${this.name} makes a sound` }
}
class Dog extends Animal {
speak() { return `${this.name} barks` }
}
new Dog('Rex').speak() // 'Rex barks'
extends wires up two chains: Dog.prototype -> Animal.prototype (instance
methods) and Dog -> Animal (statics). It's the clean syntax for the manual
prototype linking you'd otherwise write.
It calls the parent constructor, initializing the inherited part of the
instance. In a derived class you must call super() before using this.
class Dog extends Animal {
constructor(name, breed) {
super(name) // run Animal's constructor first
this.breed = breed
}
}
The parent constructor actually creates this for a derived class, which is why
super() must come first. If a subclass has no constructor, a default one
calls super(...args) automatically.
super.method() invokes the parent class's method with the current instance as
this — for extending rather than fully replacing behavior.
class Dog extends Animal {
speak() {
return super.speak() + ' (woof)' // reuse parent, add to it
}
}
super works in any method, getter, or static method — not just the
constructor. It resolves to the method on the parent's prototype, so overrides
can build on the inherited version.
In a derived class, the instance object is created by the parent constructor via
super(). Until super() runs, this is uninitialized, so accessing it throws
a ReferenceError.
class Dog extends Animal {
constructor(name) {
this.x = 1 // ReferenceError: must call super first
super(name)
}
}
This is a real semantic difference from constructor functions (where this
exists immediately). The rule ensures the parent has fully set up the object
before the child modifies it.
Defining a method in a subclass with the same name as the parent's — the subclass version shadows the parent's for instances of the subclass.
class Shape { area() { return 0 } }
class Circle extends Shape {
constructor(r) { super(); this.r = r }
area() { return Math.PI * this.r ** 2 } // overrides Shape.area
}
new Circle(2).area() // 12.56...
Lookup finds the subclass method first. You can still reach the parent's version
via super.area(). Overriding is the basis of polymorphism.
Different subclasses override the same method, and calling that method on a base-typed reference runs the actual object's version (dynamic dispatch).
class Shape { area() { return 0 } }
class Circle extends Shape { area() { return Math.PI * this.r ** 2 } }
class Square extends Shape { area() { return this.s ** 2 } }
const shapes = [new Circle(), new Square()]
shapes.forEach(s => console.log(s.area())) // each runs its own area()
JavaScript dispatches methods at runtime by the object's prototype, so a
Shape[] can hold mixed subclasses and each responds with its own behavior —
classic polymorphism.
If a subclass omits a constructor, JavaScript inserts one that forwards all
arguments to the parent: constructor(...args) { super(...args) }.
class Animal { constructor(name) { this.name = name } }
class Dog extends Animal {} // implicit: constructor(...a){ super(...a) }
new Dog('Rex').name // 'Rex' — forwarded to Animal
So you only need to write a subclass constructor when you add fields or change
how the parent is called. The default just chains to super.
Yes — in a static method, super.method() calls the parent class's static
method, because static members are inherited along the constructor's own
prototype chain.
class Base { static create() { return 'base' } }
class Derived extends Base {
static create() { return super.create() + '-derived' }
}
Derived.create() // 'base-derived'
This works because Derived.__proto__ === Base, so super in a static context
resolves to Base. Static inheritance is a real and sometimes-surprising part
of extends.
Use extends directly — modern engines support subclassing built-ins, with
super() initializing the built-in part.
class Stack extends Array {
peek() { return this[this.length - 1] }
}
const s = new Stack()
s.push(1, 2); s.peek() // 2 — inherits all Array methods
class AppError extends Error {
constructor(msg, code) { super(msg); this.name = 'AppError'; this.code = code }
}
Caveat: extending Error historically needed Object.setPrototypeOf(this, new.target.prototype) for correct instanceof after transpilation; native
ES6+ handles it. Extending Array also has subtle length/index behaviors.
JavaScript has no abstract keyword, so you simulate it: throw if instantiated
directly (new.target) and throw from methods subclasses must implement.
class Shape {
constructor() {
if (new.target === Shape) throw new Error('Shape is abstract')
}
area() { throw new Error('area() must be implemented') }
}
class Circle extends Shape {
constructor(r) { super(); this.r = r }
area() { return Math.PI * this.r ** 2 }
}
new.target === Shape blocks direct instantiation; unimplemented methods throw
at call time. It's a convention, not a language feature.
Favor composition when behavior is orthogonal or shared across unrelated types, or when a deep hierarchy would become rigid. Use inheritance for genuine is-a relationships with stable base classes.
// inheritance forcing a hierarchy
class FlyingSwimmingDuck extends Animal {}
// compose capabilities
const duck = { ...canFly(), ...canSwim(), name: 'Donald' }
Deep inheritance suffers from the fragile-base-class problem; composition (mixins, delegation, functions) stays flexible. "Favor composition over inheritance" applies strongly in JS.
super accesses parent prototype members — methods and accessors (getters/
setters) — but not instance fields (those live on this, not the prototype).
class A {
get label() { return 'A' }
x = 1 // instance field
}
class B extends A {
get label() { return super.label + 'B' } // 'AB'
getX() { return super.x } // undefined (field, not on proto)
}
Since super looks up the parent prototype, it sees inherited methods/accessors
but not own instance fields, which belong to the instance.
For new Child(): the chain of super() calls runs top-down (parent
constructor first), and fields initialize right after that level's
super() returns, before the rest of that constructor's body.
class A { constructor() { console.log('A ctor'); this.init() } init(){} }
class B extends A {
x = (console.log('B field'), 1)
constructor() { super(); console.log('B ctor') }
}
// new B(): "A ctor" -> "B field" -> "B ctor"
A classic trap: a parent constructor calling an overridable method runs the
child override before the child's fields are initialized — so the override
may see undefined.
instanceof is true for the class and all its ancestors, since each ancestor's
prototype is in the chain.
class A {}
class B extends A {}
class C extends B {}
const c = new C()
c instanceof C // true
c instanceof B // true
c instanceof A // true
So instanceof answers "is-a (or descends-from)". To check the exact class,
compare obj.constructor === C or Object.getPrototypeOf(obj) === C.prototype.
JavaScript allows only single extends, so you simulate multiple inheritance
with mixins — functions that take a base class and return an extended one,
chained together.
const Serializable = (B) => class extends B { serialize() {} }
const Comparable = (B) => class extends B { compareTo() {} }
class Model extends Serializable(Comparable(Object)) {}
Each mixin wraps the previous class, building a linear chain that combines behaviors. This is the standard pattern for "implementing multiple interfaces" of behavior in JS.
this stays the current instance — super.method() runs the parent's
method but with the child instance as this, so it operates on the child's data.
class A { describe() { return this.constructor.name } }
class B extends A {
describe() { return super.describe() } // parent method, child `this`
}
new B().describe() // 'B' — this.constructor is B
super changes which method runs, not the receiver. That's why an inherited
method can still see the subclass's overridden properties and constructor.
Redefine the accessor in the subclass. Note that defining only a getter (or only a setter) in the child can hide the inherited pair, so redefine both if needed.
class A {
get value() { return 1 }
set value(v) {}
}
class B extends A {
get value() { return super.value + 1 } // extend
// if you define only the getter, the inherited setter is shadowed away
}
Accessors are properties, so overriding one member of a get/set pair replaces the whole property descriptor — define both in the child to keep both behaviors.
No protected keyword. The conventions are: _underscore (a hint that it's
"internal," not enforced) or #private (truly private, but not accessible
to subclasses).
class Base {
_internal = 1 // convention: subclasses may use, but not enforced
#secret = 2 // private — subclasses CANNOT access this
}
#private is stricter than protected (even subclasses can't see it). For
subclass-accessible-but-not-public members, the _ convention is all JS offers.
Yes — method dispatch is dynamic, so a parent method calling this.foo() runs
the child's override if there is one. This is the template-method pattern.
class Base {
process() { return this.transform(this.read()) } // calls overrides
read() { return 'data' }
transform(x) { return x }
}
class Json extends Base {
transform(x) { return JSON.parse(x) } // parent's process() uses this
}
Because this is the actual instance, the inherited process() polymorphically
invokes the subclass's transform — powerful, but watch the constructor-order
trap.
An arrow function lexically captures super from its enclosing method, so
super.x works inside an arrow defined within a method.
class B extends A {
greet() {
const fn = () => super.greet() // super captured lexically
return fn()
}
}
Like this, super is lexical in arrows — it binds to where the arrow is
written. A regular nested function, by contrast, has no super binding.
class C extends null creates a class whose instances do not inherit from
Object.prototype — like Object.create(null) but as a class. It's an edge case
and tricky (you must manually handle the prototype in the constructor).
class NullProto extends null {
constructor() { return Object.create(new.target.prototype) }
}
Instances lack toString, hasOwnProperty, etc. It's rarely used — mainly for
creating prototype-less objects via class syntax. Most code never needs it.
Mostly cosmetic, but watch the behavioral differences: classes must be called
with new, are not usable before declaration (TDZ), run in strict mode,
and have non-enumerable methods.
// these worked with the function but not the class:
const f = MyFunc() // -> class: TypeError (needs new)
new MyClassUsedAbove() // -> ReferenceError (TDZ) if used before declaration
So code that relied on calling without new, hoisting, or sloppy-mode behavior
can break. The upside is those were usually bugs the class now catches.
Each class's super refers to its immediate parent, so a method can be
extended at each level and chained via super.
class A { greet() { return 'A' } }
class B extends A { greet() { return super.greet() + 'B' } }
class C extends B { greet() { return super.greet() + 'C' } }
new C().greet() // 'ABC'
super is statically bound to the class where the method is defined (its
[[HomeObject]]), not the runtime instance — so multi-level chains resolve
correctly even with overrides.
Instead of inheriting, a class can hold another object and delegate to it — composition over inheritance.
class Engine { start() { return 'vroom' } }
class Car {
#engine = new Engine() // has-a Engine
start() { return this.#engine.start() } // delegate
}
This avoids tight coupling to a base class and lets you swap the held object. Many designs that look like inheritance are cleaner as composition with delegation.
Practice tests are coming soon
Get notified when interactive mock interviews and quizzes launch.