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 Explained

class 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 instancesuper.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.