Inheritance with extends
The extends keyword lets one class build on another, inheriting its methods and adding or
overriding behavior. Under the hood it wires up the same prototype chain you'd construct by
hand with constructor functions — but cleanly and correctly, removing the manual steps that
used to cause bugs. If you've read how prototypal inheritance works, class ... extends is
simply the ergonomic, standardized version of it.
class Animal {
constructor(name) { this.name = name }
speak() { return `${this.name} makes a sound` }
}
class Dog extends Animal {
speak() { return `${this.name} barks` } // override
}
const d = new Dog('Rex')
d.speak() // 'Rex barks'
d instanceof Animal // true
Dog inherits Animal's constructor behavior and any methods it doesn't override. The
chain is d -> Dog.prototype -> Animal.prototype -> Object.prototype -> null.
What extends sets up
extends does two pieces of prototype wiring that you'd otherwise write manually:
- It links the instance chain:
Dog.prototype's prototype becomesAnimal.prototype, so instances inherit instance methods. - It links the static chain:
Dog's prototype becomesAnimal, so static methods are inherited too (more on that below).
Object.getPrototypeOf(Dog.prototype) === Animal.prototype // true (instance methods)
Object.getPrototypeOf(Dog) === Animal // true (static methods)
This dual linkage is something the old constructor pattern handled awkwardly; extends does
both correctly every time.
super in the constructor
When a subclass defines its own constructor, it must call super(...) before using
this. super() invokes the parent constructor, which is what actually creates and
initializes this for a derived class.
class Animal {
constructor(name) { this.name = name }
}
class Dog extends Animal {
constructor(name, breed) {
super(name) // must come first
this.breed = breed // now `this` is available
}
}
new Dog('Rex', 'Lab') // { name: 'Rex', breed: 'Lab' }
The ordering rule is strict and enforced:
class Bad extends Animal {
constructor(name) {
this.x = 1 // ReferenceError — `this` before super()
super(name)
}
}
This is because, in a derived class, this literally doesn't exist until the parent
constructor produces it. Always call super first.
The implicit constructor
If a subclass doesn't define a constructor, JavaScript inserts a default one that forwards all arguments to the parent:
class Cat extends Animal {} // implicitly: constructor(...args){ super(...args) }
new Cat('Whiskers').name // 'Whiskers' forwarded automatically
So you only need to write a constructor in the subclass when you want to add parameters or extra initialization. If you don't, inheritance "just works."
super in methods
Inside a method, super.methodName() calls the parent's version of that method — the clean
replacement for the old Parent.prototype.method.call(this) idiom. This lets you extend
rather than fully replace inherited behavior.
class Animal {
describe() { return `I am ${this.name}` }
}
class Dog extends Animal {
describe() {
return `${super.describe()} and I bark` // build on the parent
}
}
new Dog('Rex').describe() // 'I am Rex and I bark'
super in a method resolves to the parent prototype but keeps this bound to the current
instance — so super.describe() runs Animal's method with the dog's data.
Overriding methods
Overriding is just defining a method with the same name in the subclass; it shadows the
parent's version for instances of the subclass. You can fully replace or partially extend
with super.
class Shape {
area() { return 0 }
toString() { return `Shape with area ${this.area()}` }
}
class Square extends Shape {
constructor(side) { super(); this.side = side }
area() { return this.side ** 2 } // override — toString() now uses THIS area()
}
new Square(4).toString() // 'Shape with area 16' polymorphism
Notice Shape.toString calls this.area(), which resolves to Square's override — that's
polymorphism: the inherited method automatically uses the subclass's behavior.
Inheriting static methods
Static methods are inherited too, thanks to the static side of the chain that extends sets
up. A subclass can call its parent's static methods, and super works in static methods as
well.
class Model {
static create(data) { return new this(data) } // `this` is the calling class
}
class User extends Model {}
User.create({ name: 'Ada' }) instanceof User // true `this` is User
Because this in a static method is the class it was called on, Model.create builds a
User when invoked as User.create — a powerful pattern for factory methods.
Extending built-ins
You can extend built-in classes like Array, Error, or Map.
class Stack extends Array {
peek() { return this[this.length - 1] }
}
const s = new Stack()
s.push(1, 2, 3)
s.peek() // 3
Custom Error subclasses are especially common for typed error handling. One caveat:
extending some built-ins has historical quirks when transpiled to ES5, but in modern
environments it works as expected.
Common pitfalls
A few mistakes recur with class inheritance:
// using this before super()
class A extends Base { constructor() { this.x = 1; super() } }
// forgetting super() entirely in a derived constructor
class B extends Base { constructor() { /* no super */ } } // ReferenceError on use
// assuming super refers to the parent INSTANCE — it refers to the parent PROTOTYPE
The mental model that prevents all three: in a derived class, this is manufactured by
super(), so it can't precede it; and super.x() means "the parent's method, run with my
this," not "a separate parent object."
Key takeaways
extendslinks both the instance prototype chain and the static chain — full inheritance, correctly wired.- A derived constructor must call
super(...)before touchingthis; omit the constructor entirely to auto-forward arguments. super.method()calls the parent's method with the currentthis, enabling extend-not- replace overrides.- Overriding plus methods that call
this.other()give you polymorphism for free. - Static methods are inherited;
thisin a static method is the calling class, enabling factory patterns. - You can extend built-ins like
ArrayandError; just callsuper()correctly.
extends and super are the polished front-end of prototypal inheritance. Knowing what they
compile to keeps every edge case predictable.