Classical inheritance in JavaScript is set up by linking one constructor's prototype to another's, so instances of the child inherit the parent's methods and satisfy instanceof for both. Before ES6 added class and extends, you wired this by hand — and class Child extends Parent still desugars to exactly these steps. Implement es5Extends(Child, Parent) so that, after you call it, Child inherits from Parent the classic prototypal way: no class, no extends. See MDN on inheritance and the prototype chain.
es5Extends(Child: Function, Parent: Function): Function
// mutates Child so its instances inherit from Parent; returns Child
function Animal(name) {
this.name = name;
}
Animal.prototype.describe = function () {
return this.name + ' is an animal';
};
Animal.prototype.legs = function () {
return 4;
};
function Dog(name, breed) {
Animal.call(this, name); // run the parent constructor on `this` (the ES5 super call)
this.breed = breed;
}
es5Extends(Dog, Animal); // wire inheritance BEFORE adding Dog's own methods
Dog.prototype.describe = function () {
// extend the parent method by reaching it through the prototype
return Animal.prototype.describe.call(this) + ' (a dog)';
};
const d = new Dog('Rex', 'pug');
d.name; // 'Rex' — set by Animal.call
d.legs(); // 4 — inherited from Animal.prototype
d.describe(); // 'Rex is an animal (a dog)'
d instanceof Dog; // true
d instanceof Animal; // true
d.constructor === Dog; // true — the constructor pointer is restored
// Inheritance is one-directional, and the link must NOT run the parent:
new Animal('Spot') instanceof Dog; // false
function Base(required) {
if (required === undefined) throw new Error('needs an argument');
this.required = required;
}
function Derived() {
Base.call(this, 'ok');
}
es5Extends(Derived, Base); // must NOT throw — the link is Object.create, not new Base()
Child.prototype = Object.create(Parent.prototype) so a lookup that misses on the instance walks up to Parent.prototype. This is what makes inherited methods and instanceof Parent work.new Parent() — that runs the parent constructor with no arguments (it may throw) and copies stray fields onto the shared prototype. Use Object.create instead.constructor — replacing Child.prototype wipes its constructor, so set Child.prototype.constructor = Child or instance.constructor wrongly points at Parent.es5Extends — the child constructor runs the parent's with Parent.call(this, ...args); a child method reuses the parent's with Parent.prototype.method.call(this).Child itself delegate to Parent with Object.setPrototypeOf, so the parent's static members are reachable on Child.class / extends — reproduce the wiring by hand; that is the exercise.You'll write the helper that class Child extends Parent compiles down to: link the two prototypes so children inherit the parent's methods, put the constructor pointer back, and pass the parent's static members down.
Long before class, JavaScript already had inheritance — it just lived entirely in the prototype chain, and you wired it up by hand. es5Extends(Dog, Animal) should make every Dog behave like an Animal too: it inherits Animal's prototype methods, it is instanceof Animal, and its own name field still gets set. Interviewers ask this to check whether you can explain what extends actually does under the hood — because it desugars to exactly these few lines.
Inheritance in JavaScript is a chain of objects. When you read dog.legs, the engine looks on the instance, then on Dog.prototype, then on whatever Dog.prototype is linked to, and so on until it finds legs or hits null. class Child extends Parent sets up that chain in two places: the instance side (Dog.prototype links to Animal.prototype) and the static side (Dog links to Animal). es5Extends builds the same links.
The instinct is to make Child.prototype an actual instance of the parent, so it literally is a parent:
function es5Extends(Child, Parent) {
Child.prototype = new Parent(); // link by making a Parent instance
}
This even looks reasonable — new Parent() produces an object whose prototype is Parent.prototype, so the chain does get connected. But it breaks in two ways. First, it runs the parent constructor with no arguments: if Animal did this.name = name.toUpperCase(), or checked for a required argument, it throws right here and you can't even extend. Second, every field the constructor assigns — this.name = undefined — becomes a real own property on the shared prototype, so every Dog inherits a stray name of undefined. (The cousin bug, Child.prototype = Parent.prototype, is worse still: now the two share one object, so Dog.prototype.speak = ... also bolts speak onto Animal.)
What you actually want is a fresh, empty object that sits on the chain purely to point at Parent.prototype. That is exactly what Object.create(Parent.prototype) gives you.
function es5Extends(Child, Parent) {
if (typeof Child !== 'function' || typeof Parent !== 'function') {
throw new TypeError('es5Extends: both Child and Parent must be constructor functions');
}
// 1. Link the instance chain. Object.create makes a fresh empty object whose
// [[Prototype]] is Parent.prototype — WITHOUT ever running Parent.
Child.prototype = Object.create(Parent.prototype);
// 2. Restore the constructor pointer. Step 1 threw away the old prototype
// object, so `constructor` now resolves up to Parent; point it back at Child.
Child.prototype.constructor = Child;
// 3. Link the static chain, so Parent's static members are reachable on Child.
// This is the static half of `class Child extends Parent`.
Object.setPrototypeOf(Child, Parent);
return Child; // convenience — lets callers write `const Dog = es5Extends(Dog, Animal)`
}
module.exports = { es5Extends };
Three lines do the real work:
Object.create(Parent.prototype) is the fix for the naive version. It builds a brand-new object and sets its prototype to Parent.prototype in one step, without calling Parent. That link is what makes inherited methods and instanceof Parent work.Child.prototype.constructor = Child repairs a detail the previous line broke. A prototype object normally has a constructor property pointing back at its function; by replacing the whole prototype we lost it, so new Dog().constructor would resolve up to Animal. Setting it back keeps dog.constructor === Dog.Object.setPrototypeOf(Child, Parent) handles static inheritance — members hung directly on the parent function, like Animal.create. It makes the Child function itself delegate to Parent, so Dog.create finds Animal.create. It is the exact static-side link a real class extends installs.Notice what es5Extends does not do: it never touches instance fields like name or breed. Those are set per-instance, and that job belongs to the child constructor — the super call.
es5Extends links the shared, class-level machinery: prototype methods and statics. But each instance's own fields have to be filled in every time you construct one, and a child method often wants to build on the parent's version. Both are done with .call(this) — the ES5 spelling of super.
Dog, Animal.call(this, name) invokes Animal's body with this set to the Dog being built, so this.name = name writes name onto the instance. This is super(name).Dog.prototype.describe, Animal.prototype.describe.call(this) runs the parent's describe with the same this, returns 'Rex is an animal', and the child appends ' (a dog)'. This is super.describe().Both reach the parent's code directly and pass this explicitly, because a plain Animal(name) or describe() call would lose the instance.
Take the Animal / Dog pair from the examples and trace new Dog('Rex', 'pug'):
es5Extends(Dog, Animal) already ran, so Dog.prototype is a fresh object linked to Animal.prototype, Dog.prototype.constructor is Dog, and Dog itself delegates to Animal.new Dog('Rex', 'pug') creates an object whose prototype is Dog.prototype, then runs the Dog body with this as that object.Animal.call(this, 'Rex') runs Animal's body against the instance: this.name = 'Rex'.this.breed = 'pug' adds the child's own field. The instance now owns { name: 'Rex', breed: 'pug' }.dog.legs() is not on the instance or on Dog.prototype, so lookup walks to Animal.prototype.legs and returns 4.dog.describe() finds describe on Dog.prototype first (the override). It calls Animal.prototype.describe.call(this) for 'Rex is an animal', then appends ' (a dog)' for 'Rex is an animal (a dog)'.dog instanceof Dog and dog instanceof Animal are both true, because both prototypes sit on the chain; dog.constructor is Dog.new Parent() — runs the parent constructor with no arguments (it may throw) and copies junk own properties onto the prototype. Use Object.create(Parent.prototype), which links a fresh empty object and never calls Parent.constructor fixup — after Child.prototype = Object.create(Parent.prototype), new Child().constructor is Parent, which surprises anything that reads .constructor. Restore it with Child.prototype.constructor = Child.es5Extends replaces Child.prototype, so any method you put on Child.prototype before calling it is thrown away. Call es5Extends(Child, Parent) first, then add Child.prototype.foo.Dog never calls Animal.call(this, name), instances inherit Animal's methods but have no name, so describe() reads undefined. Inheriting methods and initializing fields are two separate jobs.this in a super-method call — Animal.prototype.describe() with no .call(this) runs with the wrong this and reads the wrong object. Always thread the instance through with .call(this) (or .apply(this, args)).Child.prototype = Object.create(...) links instances but not the functions themselves, so Parent.staticMethod is invisible on Child. Object.setPrototypeOf(Child, Parent) adds that link.Reflect and native super — inside a real class, super.method() compiles to a Reflect.get off the prototype's prototype bound to the current this, and super(...) uses Reflect.construct so new.target flows through. .call(this) is the hand-rolled version of the same idea.constructor — native classes define constructor as non-enumerable. A faithful es5Extends would use Object.defineProperty(Child.prototype, 'constructor', { value: Child, enumerable: false, writable: true, configurable: true }) instead of a plain assignment, so it never shows up in a for...in loop.class extends null — the language allows a parent of null (an instance whose prototype chain skips Object.prototype). Supporting it means special-casing Object.create(null), a rare edge worth knowing exists.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Classical inheritance in JavaScript is set up by linking one constructor's prototype to another's, so instances of the child inherit the parent's methods and satisfy instanceof for both. Before ES6 added class and extends, you wired this by hand — and class Child extends Parent still desugars to exactly these steps. Implement es5Extends(Child, Parent) so that, after you call it, Child inherits from Parent the classic prototypal way: no class, no extends. See MDN on inheritance and the prototype chain.
es5Extends(Child: Function, Parent: Function): Function
// mutates Child so its instances inherit from Parent; returns Child
function Animal(name) {
this.name = name;
}
Animal.prototype.describe = function () {
return this.name + ' is an animal';
};
Animal.prototype.legs = function () {
return 4;
};
function Dog(name, breed) {
Animal.call(this, name); // run the parent constructor on `this` (the ES5 super call)
this.breed = breed;
}
es5Extends(Dog, Animal); // wire inheritance BEFORE adding Dog's own methods
Dog.prototype.describe = function () {
// extend the parent method by reaching it through the prototype
return Animal.prototype.describe.call(this) + ' (a dog)';
};
const d = new Dog('Rex', 'pug');
d.name; // 'Rex' — set by Animal.call
d.legs(); // 4 — inherited from Animal.prototype
d.describe(); // 'Rex is an animal (a dog)'
d instanceof Dog; // true
d instanceof Animal; // true
d.constructor === Dog; // true — the constructor pointer is restored
// Inheritance is one-directional, and the link must NOT run the parent:
new Animal('Spot') instanceof Dog; // false
function Base(required) {
if (required === undefined) throw new Error('needs an argument');
this.required = required;
}
function Derived() {
Base.call(this, 'ok');
}
es5Extends(Derived, Base); // must NOT throw — the link is Object.create, not new Base()
Child.prototype = Object.create(Parent.prototype) so a lookup that misses on the instance walks up to Parent.prototype. This is what makes inherited methods and instanceof Parent work.new Parent() — that runs the parent constructor with no arguments (it may throw) and copies stray fields onto the shared prototype. Use Object.create instead.constructor — replacing Child.prototype wipes its constructor, so set Child.prototype.constructor = Child or instance.constructor wrongly points at Parent.es5Extends — the child constructor runs the parent's with Parent.call(this, ...args); a child method reuses the parent's with Parent.prototype.method.call(this).Child itself delegate to Parent with Object.setPrototypeOf, so the parent's static members are reachable on Child.class / extends — reproduce the wiring by hand; that is the exercise.You'll write the helper that class Child extends Parent compiles down to: link the two prototypes so children inherit the parent's methods, put the constructor pointer back, and pass the parent's static members down.
Long before class, JavaScript already had inheritance — it just lived entirely in the prototype chain, and you wired it up by hand. es5Extends(Dog, Animal) should make every Dog behave like an Animal too: it inherits Animal's prototype methods, it is instanceof Animal, and its own name field still gets set. Interviewers ask this to check whether you can explain what extends actually does under the hood — because it desugars to exactly these few lines.
Inheritance in JavaScript is a chain of objects. When you read dog.legs, the engine looks on the instance, then on Dog.prototype, then on whatever Dog.prototype is linked to, and so on until it finds legs or hits null. class Child extends Parent sets up that chain in two places: the instance side (Dog.prototype links to Animal.prototype) and the static side (Dog links to Animal). es5Extends builds the same links.
The instinct is to make Child.prototype an actual instance of the parent, so it literally is a parent:
function es5Extends(Child, Parent) {
Child.prototype = new Parent(); // link by making a Parent instance
}
This even looks reasonable — new Parent() produces an object whose prototype is Parent.prototype, so the chain does get connected. But it breaks in two ways. First, it runs the parent constructor with no arguments: if Animal did this.name = name.toUpperCase(), or checked for a required argument, it throws right here and you can't even extend. Second, every field the constructor assigns — this.name = undefined — becomes a real own property on the shared prototype, so every Dog inherits a stray name of undefined. (The cousin bug, Child.prototype = Parent.prototype, is worse still: now the two share one object, so Dog.prototype.speak = ... also bolts speak onto Animal.)
What you actually want is a fresh, empty object that sits on the chain purely to point at Parent.prototype. That is exactly what Object.create(Parent.prototype) gives you.
function es5Extends(Child, Parent) {
if (typeof Child !== 'function' || typeof Parent !== 'function') {
throw new TypeError('es5Extends: both Child and Parent must be constructor functions');
}
// 1. Link the instance chain. Object.create makes a fresh empty object whose
// [[Prototype]] is Parent.prototype — WITHOUT ever running Parent.
Child.prototype = Object.create(Parent.prototype);
// 2. Restore the constructor pointer. Step 1 threw away the old prototype
// object, so `constructor` now resolves up to Parent; point it back at Child.
Child.prototype.constructor = Child;
// 3. Link the static chain, so Parent's static members are reachable on Child.
// This is the static half of `class Child extends Parent`.
Object.setPrototypeOf(Child, Parent);
return Child; // convenience — lets callers write `const Dog = es5Extends(Dog, Animal)`
}
module.exports = { es5Extends };
Three lines do the real work:
Object.create(Parent.prototype) is the fix for the naive version. It builds a brand-new object and sets its prototype to Parent.prototype in one step, without calling Parent. That link is what makes inherited methods and instanceof Parent work.Child.prototype.constructor = Child repairs a detail the previous line broke. A prototype object normally has a constructor property pointing back at its function; by replacing the whole prototype we lost it, so new Dog().constructor would resolve up to Animal. Setting it back keeps dog.constructor === Dog.Object.setPrototypeOf(Child, Parent) handles static inheritance — members hung directly on the parent function, like Animal.create. It makes the Child function itself delegate to Parent, so Dog.create finds Animal.create. It is the exact static-side link a real class extends installs.Notice what es5Extends does not do: it never touches instance fields like name or breed. Those are set per-instance, and that job belongs to the child constructor — the super call.
es5Extends links the shared, class-level machinery: prototype methods and statics. But each instance's own fields have to be filled in every time you construct one, and a child method often wants to build on the parent's version. Both are done with .call(this) — the ES5 spelling of super.
Dog, Animal.call(this, name) invokes Animal's body with this set to the Dog being built, so this.name = name writes name onto the instance. This is super(name).Dog.prototype.describe, Animal.prototype.describe.call(this) runs the parent's describe with the same this, returns 'Rex is an animal', and the child appends ' (a dog)'. This is super.describe().Both reach the parent's code directly and pass this explicitly, because a plain Animal(name) or describe() call would lose the instance.
Take the Animal / Dog pair from the examples and trace new Dog('Rex', 'pug'):
es5Extends(Dog, Animal) already ran, so Dog.prototype is a fresh object linked to Animal.prototype, Dog.prototype.constructor is Dog, and Dog itself delegates to Animal.new Dog('Rex', 'pug') creates an object whose prototype is Dog.prototype, then runs the Dog body with this as that object.Animal.call(this, 'Rex') runs Animal's body against the instance: this.name = 'Rex'.this.breed = 'pug' adds the child's own field. The instance now owns { name: 'Rex', breed: 'pug' }.dog.legs() is not on the instance or on Dog.prototype, so lookup walks to Animal.prototype.legs and returns 4.dog.describe() finds describe on Dog.prototype first (the override). It calls Animal.prototype.describe.call(this) for 'Rex is an animal', then appends ' (a dog)' for 'Rex is an animal (a dog)'.dog instanceof Dog and dog instanceof Animal are both true, because both prototypes sit on the chain; dog.constructor is Dog.new Parent() — runs the parent constructor with no arguments (it may throw) and copies junk own properties onto the prototype. Use Object.create(Parent.prototype), which links a fresh empty object and never calls Parent.constructor fixup — after Child.prototype = Object.create(Parent.prototype), new Child().constructor is Parent, which surprises anything that reads .constructor. Restore it with Child.prototype.constructor = Child.es5Extends replaces Child.prototype, so any method you put on Child.prototype before calling it is thrown away. Call es5Extends(Child, Parent) first, then add Child.prototype.foo.Dog never calls Animal.call(this, name), instances inherit Animal's methods but have no name, so describe() reads undefined. Inheriting methods and initializing fields are two separate jobs.this in a super-method call — Animal.prototype.describe() with no .call(this) runs with the wrong this and reads the wrong object. Always thread the instance through with .call(this) (or .apply(this, args)).Child.prototype = Object.create(...) links instances but not the functions themselves, so Parent.staticMethod is invisible on Child. Object.setPrototypeOf(Child, Parent) adds that link.Reflect and native super — inside a real class, super.method() compiles to a Reflect.get off the prototype's prototype bound to the current this, and super(...) uses Reflect.construct so new.target flows through. .call(this) is the hand-rolled version of the same idea.constructor — native classes define constructor as non-enumerable. A faithful es5Extends would use Object.defineProperty(Child.prototype, 'constructor', { value: Child, enumerable: false, writable: true, configurable: true }) instead of a plain assignment, so it never shows up in a for...in loop.class extends null — the language allows a parent of null (an instance whose prototype chain skips Object.prototype). Supporting it means special-casing Object.create(null), a rare edge worth knowing exists.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.