Inheritance remains one of the most relied upon and misunderstood features of JavaScript to this day. Since ES2015 JavaScript developers have been able to ignore how the inheritance sausage is made by relying on the class syntax that hides the nitty gritty details, until they run into its mind-bending edge cases.
In this post we'll explore the secrets of JavaScript inheritance: [[Prototype]] and constructors.
But first, put your knowledge to the test:
How many can you get right?
1. Overriding getters and setters
console.log('Overriding getters and setters');
class SuperClass {
_value = undefined;
get value() { return this._value; }
}
class SubClass extends SuperClass {
set value(to) { this._value = to; }
}
const sub = new SubClass();
sub.value = 5;
// What gets logged?
console.log(sub.value); // undefined
2. Deleting from a class instance
console.log('Deleting from a class instance');
class MyClass {
fn1 = function() {}
fn2() {}
}
const myInstance = new MyClass();
// What gets logged?
delete myInstance.fn1;
console.log(myInstance.fn1); // undefined
delete myInstance.fn2;
console.log(myInstance.fn2); // fn2() {}
3. Deleting from an object
console.log('Deleting from an object');
const myObject = {
fn() {},
toString() {},
};
// What gets logged?
delete myObject.fn;
console.log(myObject.fn); // undefined
console.log(myObject.toString); // toString() {}
myObject.toString = undefined
console.log(myObject.toString); // undefined
delete myObject.toString;
console.log(myObject.toString); // toString() { [native code] }
4. Overriding constructors???
class MyClass {
constructor() {
console.log("Original Consturctor");
}
}
MyClass.prototype.constructor = function Overridden() {
console.log("Overridden Constructor");
}
// What gets logged?
const instance = new MyClass(); // "Original Constructor"
console.log(instance.constructor.name); // Overridden
console.log(instance.constructor.prototype === Object.getPrototypeOf(instance)); // false
If you got all of the above right then maybe you're already grizzled JavaScript veteran and know all the ins and outs of OOJS (Object Oriented JavaScript).
For the rest of us, it's time to open Pandora's Box.
Inheritance
In OOP (Object Oriented Programming), inheritance is the mechanism used build a new object or class ontop another object or class.
JavaScript has inheritance but doesn't have static "classes" like static OO languages (C++, C#, Java). Instead, JavaScript links objects together by prototypes. Even in ES2015, class is mostly just syntactic sugar for objects with prototypal relationships.
At a glance, OOJS using class appears sane.
class Base {
prop = 'hello world';
}
class Sub extends Base {
//
}
const sub = new Sub();
// sub has access to properties on base
console.log(sub.prop); // "hello world"
But how does this really work? What is a "class" and how does sub have access to prop?
Enter: [[Prototype]]
JavaScript uses prototypes to achieve inheritance. All objects have a [[Prototype]] internal slot which is the object being inherited from. Internal slots are internal to the JavaScript interpreter. Some internal slots are exposed via functions like Object.getPrototypeOf() and many aren't exposed at all.
An object's [[Prototype]] can be null or another object which itself has a [[Prototye]] slot. An object's linked list of [[Prototype]]s (i.e. myObject.[[Prototype]].[[Prototype]].[[Prototype]]...) is called its "prototype chain" and terminates with null.
To lookup a property on an object the JavaScript interpreter performs a lookup on the top-level object, then that object's [[Prototype]], then [[Prototype]].[[Prototype]], and so on until reaching null.
We can use Object.create(proto) to create a new object with proto as its [[Prototype]] and use Object.getPrototypeOf(obj) to get the [[Prototype]] of an object obj
const ancestor = Object.create(null);
const parent = Object.create(ancestor);
const child = Object.create(parent);
// child inherits from parent
console.log(Object.getPrototypeOf(child) === parent); // true
// parent inherits from ancestor
console.log(Object.getPrototypeOf(parent) === ancestor); // true
// ancestor inherits nothing
console.log(Object.getPrototypeOf(ancestor) === null); // true
We can also use Object.setPrototypeOf(sub, base) to change the [[Prototype]] of an object sub to another object (or null), base. Notice - unlike static OO languages we can dynamically change inheritance heirarchies at runtime! For performance reasons this is strongly advised against. According to Benedikt Muerer of v8, a every time you change the prototype chain, a kitten dies.
const base = { prop: 'hello world' };
const sub = {};
console.log(sub.prop); // undefined
Object.setPrototypeOf(sub, base);
console.log(sub.prop); // "hello world"
Object.setPrototypeOf(sub, null);
console.log(sub.prop); // undefined
Objects created using the object literal syntax {} inherit from JavaScript's base Object.prototype which in-turn inherits from null.
const obj = {};
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype) === null); // true
Functions
Functions are a regular JavaScript objects, but with additional internal slots. Like regular objects they have properties and a [[Prototype]] internal slot, but unlike other objects they are callable thanks to a [[Call]] internal method.
Constructors are functions with some specific attributes.
Enter: Constructors
Constructor functions compliment prototypes by making prototype configuration and object creation and inialisation easy and consistent. Inheritance can still be achieved without constructors (for example with Object.create) but it's less common.
Any non-arrow function (any function created with the function keyword) can be used as a constructor. All non-arrow functions have a prototype property, initialized to a new object with only one property prototype.constructor whose value is the constructor function. Note that a function's prototype property is NOT the same as that functions [[Prototype]] internal slot.
Constructors have to be called with a the new operator (unless being used within another constructor function for inheritance) for the this variable to be created and bound correctly. The this object's [[Prototype]] is set to the constructors prototype property.
It's good practice to begin constructor names with an uppercase character so you know to call them with new.
function Constructor() {}
console.log(Constructor.prototype); // { constructor: f }
const instance = new Constructor();
console.log(Object.getPrototypeOf(instance) === Constructor.prototype) // true
// i.e. instance.[[Prototype]] === Constructor.prototype
When called with new, construtors implicitly return their this object.
let this_ref;
function Constructor() {
console.log(Object.getPrototypeOf(this) === Constructor.prototype); // true
this_ref = this;
// implicitly returns `this`
}
const that = new Constructor();
console.log(that === this_ref); // true;
"classes" created with the ES2015 (e.g. class MyClass {...}) are also simply constructor functions (typeof MyClass === 'function') but whose internal slots are configured differently, such as [[IsClassConstructor]] that causes classes to throw a TypeError if called without the new operator, unlike constructor functions not created with the class syntax.
Given that instances created with the new operator inherit from their constructors prototype property, we can create functions on the prototype property that will be inherited by the instances.
function Person() {
//
}
Person.prototype.sayHello = function() {
console.log('hello');
}
const person = new Person();
person.sayHello(); // 'hello'
ES2015 classes without ES2015 syntax
Now that we know about prototypes and constructors we can replicate the ES2015 class functionality with constructor functions and prototypes.
Using constructor-prototype syntax we have enormous flexibility in how we glue together our objects at the price of having to glue them together manually.
We can manually accomplish what the ES2015 class syntax does for us by maintaining the following:
-
Instance prototype chain:
SubClass.prototype.[[Prototype]]must be set toSuperClass.prototype. This sets up the prototype chain of instances constructed fromnew SubClass(...)such that:-
subclass_instance.[[Prototype]]=== SubClass.prototype -
subclass_instance.[[Prototype]][[Prototype]]=== SuperClass.prototype -
subclass_instance.[[Prototype]][[Prototype]][[Prototype]]=== Object.prototype -
subclass_instance.[[Prototype]][[Prototype]][[Prototype]][[Prototype]]=== null
-
-
Constructor prototype chain:
SubClass.[[Prototype]]must be set toSuperClass. This means theSubClassfunction inherits "static" properties fromSuperClass(properties on the SuperClass constructor function) such that:SuperClass.staticProperty = 5SubClass.staticProperty === 5
-
Initialisation: When the
SubClassconstructor is called withnew, it needs to immediately call theSuperClassconstructor function binding itsthisvalue (SuperClass.call(this, ...)), in order to initialiseSuperClassonthisproperly.- The ES2015
classsyntax forces us to call the super constructor usingsuper()at the beginning of our subclasses constructor function, or else the interpreter will throw an error. This is not forced in constructor-prototype syntax so we need to remember it ourselves! Otherwise our class instances will not be properly initialised.
- The ES2015
Our object relations for the model described above are:
Don't be intimidated by the number of objects and connections - if you can grok the diagram then you can derive an understanding of everything relating OOJS.
The super Problem
The only class functionality we can't exactly replicate with constructors and prototypes is super.
function Base() {}
Base.prototype.fn = function() {
console.log('base');
}
function AnotherBase() {}
AnotherBase.prototype.fn = function() {
console.log('another base');
}
function Sub() {}
Object.setPrototypeOf(Sub, Base);
Sub.prototype.fn = function() {
console.log('sub');
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// "super" call, hardcoded to `Base`
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
Base.prototype.fn.call(this);
}
const sub = new Sub();
sub.fn();
// sub
// base
Object.setPrototypeOf(Sub, AnotherBase);
Object.setPrototypeOf(Sub.prototype, AnotherBase.prototype);
sub.fn();
// sub
// base
Without referencing the superclass, Base, directly we have no way to determine where the current method under invocation sits in the prototype chain, and therefore can't lookup functions that are strictly higher in the prototype chain (i.e. a super call).
By referencing Base directly in an attempt to replicate super, we've destroyed our ability to safely change the prototype since our "super" call would be referencing a function we no longer inherit.
With ES2015, we have a super keyword that still works when we reassign [[Prototype]]
class Base {
fn() {
console.log('base');
}
}
class AnotherBase {
fn() {
console.log('another base');
}
}
class Sub extends Base {
fn() {
console.log('sub');
super.fn();
}
}
const sub = new Sub();
sub.fn();
// sup
// base
Object.setPrototypeOf(Sub, AnotherBase);
Object.setPrototypeOf(Sub.prototype, AnotherBase.prototype);
sub.fn();
// sup
// another base
Pre ES2015 classes by example
We'll code a simple inheritance example of 2 classes: a superclass Animal and subclass Dog using the relations described above. Each inheritance layer has 3 associated objects: the constructor function, prototype object and instance object.
Our domain is:
In JavaScript, our objects will be:
/**
* @constructor Animal
* @abstract
*
* @param {number} legs
*/
function Animal(legs) {
this.legs = legs;
}
/**
* Abstract static property on Animal constructor
* to be overridden by a property the subclasses constructor
*
* @abstract
* @static
* @type {string}
*/
Animal.species = undefined;
/**
* getter on the animal prototype that retrieves the static, overridden
* property from the subclasses constructor, `species`
*
* @readonly
* @type {string}
*
* @example
* const dog = new Dog()
* dog.species; // calls `Animal.prototype.species` -> `Dog.species`
*/
Object.defineProperty(Animal.prototype, 'species', {
enumerable: true,
configurable: false,
/** @returns {string} */
get() {
// alternatively, `const SubClass = this.constructor`
const SubClass = Object.getPrototypeOf(this).constructor;
return SubClass.species;
},
})
/**
* Method on the Animal prototype, inherited by animal instances and subclasses
* of Animal
*
* @param {string} food
*/
Animal.prototype.eat = function(food) {
console.log(`Yum! eating ${food}`);
}
/**
* @constructor Dog
*
* Subclass of Animal
*/
function Dog() {
const legs = 4;
// we run the inherited constructor, bound to `this`, to initialise our superclass properly
// this effectively "subtypes" `this` to an instance of the superclass (`this` becomes a superset of the superclasses instances type)
Animal.call(this, legs);
}
// Inherit staticically from Animal
Object.setPrototypeOf(Dog, Animal);
// Inherit prototype from Animal
Object.setPrototypeOf(Dog.prototype, Animal.prototype);
/**
* @override
* @type {string}
*/
Dog.species = 'Dog';
/**
* Override the `eat` method from `Animal.prototype`
* Also call the super method
*
* @override
*
* @param {*} food
*/
Dog.prototype.eat = function(food) {
console.log('Woof!');
// super call!
Animal.prototype.eat.call(this, food);
}
const dog = new Dog();
dog.eat('chicken');
// 'Woof!'
// 'Yum! eating chicken'
console.log(dog.species);
// 'Dog'
Access to inherited properties
One of the most important things to understand when working directly with prototypes is how accessors and operators propagate. Of the following actions, only the get accessor propagates up the prototype chain.
| accessor or operator | propagates up the prototype chain |
|---|---|
| get | yes |
| set | no |
| delete | no |
const base = { prop: 'hello', ref: {} };
const sub = {};
Object.setPrototypeOf(sub, base);
console.log(sub.prop); // 'hello'
// the `delete` operator does not propagate
// calling delete on `prop` can have no effect on objects in its prototype chain
delete sub.prop;
console.log(sub.prop); // 'hello'
// similarly, the `set` accessor does not propagate
console.log(sub.ref === base.ref); // true
base.ref = { a: 'different', object: true };
console.log(sub.ref === base.ref); // true
sub.ref = { something: 'else' };
console.log(sub.ref === base.ref); // false
Who cares?
Most JavaScript application developers don't need to know its inheritance mechanism in great detail. Some of JavaScript's most flexible features, including prototype hacking, are considered footgun's to be avoided. If you feel the need to hack a prototype chain you're probably better off finding another way.
Knowing about prototypes is more important when working in the broader ecosystem with packages or tooling or when monkeypatching libraries (modifying prototypes of objects from third party libraries).
How does TypeScript fit into this?
Unfortunately, like a square peg into a round hole.
TypeScript doesn't attempt to model the fine details of OOJS. It doesn't differentiate between properties on a class instance and properties on a classes prototype.
class MyClass {
instanceProperty: number;
prototypeProperty() {};
constructor() { this.instanceProperty = 5; }
}
// TypeScript sees instances of MyClass as equivalent to:
interface MyClassInstance {
instanceProperty: number;
prototypeProperty() {};
}
// properties of the prototype and instance are merged together
Moreover, TypeScript doesn't even allow adding new signature to a constructor function.
const MyConstructor: { new(): {} } = function() {}
// Type '() => void' is not assignable to type 'new () => {}'.
To use TypeScript on constructor functions have to resort to the unsafe as unknown hack. The language server also won't tell us when our prototype is missing properties
interface MyInstanceAndPrototype {
//
methodOnPrototype() {};
}
interface MyConstructor extends Function {
new(): MyInstanceAndPrototype;
prototype: MyInstanceAndPrototype;
}
const MyConstructor = function MyConstructor() {} as unknown as MyConstructor
// Forgot to add `MyConstructor.prototype.methodOnPrototype`?
// There won't be any TypeScript error
Revisiting our examples
With our understanding of prototypes, constructors and property access, we can revisit our and understand initial examples
Explanation: 1. Overriding getters and setters
console.log('Overriding getters and setters');
class SuperClass {
_value = undefined;
get value() { return this._value; }
}
class SubClass extends SuperClass {
set value(to) { this._value = to; }
}
const sub = new SubClass();
sub.value = 5;
// What gets logged?
console.log(sub.value); // undefined
What went wrong?
Writing this in pre-ES2015 syntax we have something close to:
console.log('Overriding getters and setters');
function SuperClass() {
this._value = undefined;
}
Object.defineProperty(SuperClass.prototype, 'value', {
get() { return this._value },
})
function SubClass() {}
Object.setPrototypeOf(SubClass, SuperClass);
Object.setPrototypeOf(SubClass.prototype, SuperClass.prototype);
Object.defineProperty(SubClass.prototype, 'value', {
set(to) { this._value = to; },
});
const sub = new SubClass();
sub.value = 5;
// What gets logged?
console.log(sub.value); // undefined
Notice we have both SubClass.prototype.value and SuperClass.prototype.vaue.
SubClass.prototype.value overrides SuperClass.prototype.value. SubClass.prototype.value has a setter with NO GETTER!! When we read sub.value, we accessing SubClass.prototype.value which has no getter and a value of undefined by default, and therefore returns undefined. We never reach SuperClass.prototype.value! This issue once cost me 4 hours in debugging hell.
Explanation: 2. Deleting from a class instance
console.log('Deleting from a class instance');
class MyClass {
fn1 = function() {}
fn2() {}
}
const myInstance = new MyClass();
// What gets logged?
delete myInstance.fn1;
console.log(myInstance.fn1); // undefined
delete myInstance.fn2;
console.log(myInstance.fn2); // fn2() {}
Writing this in pre-ES2015 syntax we have something close to:
console.log('Deleting from a class instance');
function MyClass() {
this.fn1 = function() {};
}
MyClass.prototype.fn2 = function fn2() {}
const myInstance = new MyClass();
// What gets logged?
delete myInstance.fn1;
console.log(myInstance.fn1); // undefined
delete myInstance.fn2;
console.log(myInstance.fn2); // fn2() {}
Notice that with class syntax, setting property = ... within the class body is roughly equivalent setting this.property = ... within the classes constructor. It places the property on the class instances.
Conversely, fn2() {} within the class body adds that function to the classes prototype MyClass.prototype.
The delete operator does not propagate up the prototype chain. Therefore we delete fn1 since its on the class instance, but not fn2 since it's on the class prototype.
Explanation: 3. Deleting from an object
console.log('Deleting from an object');
const myObject = {
fn() {},
toString() {},
};
// What gets logged?
delete myObject.fn;
console.log(myObject.fn); // undefined
console.log(myObject.toString); // toString() {}
myObject.toString = undefined
console.log(myObject.toString); // undefined
delete myObject.toString;
console.log(myObject.toString); // toString() { [native code] }
Similar to 2., but now we have an object instance myObject with two functions. All objects created with the literal syntax {} have their [[Prototype]] equal to Object.prototype. Object.prototype has a toString method.
In our example:
- we override
Object.prototype.toStringin the assignment ofmyObject.- logging
myObject.toStringprints our overridden copy,toString() {}
- logging
- we set
myObject.toString = undefined, which continues to overrideObject.prototype.toStringbut now with a value of undefined.- logging
myObject.toStringprints our overridden copy,undefined
- logging
- we delete
toStringfrommyObject. nowtoStringcalls will propagate up the prototype chain.- logging
myObject.toStringprintsObject.prototype.toString.
- logging
Explanation: 4. Overriding constructors???
class MyClass {
constructor() {
console.log("Original Consturctor");
}
}
MyClass.prototype.constructor = function Overridden() {
console.log("Overridden Constructor");
}
// What gets logged?
const instance = new MyClass(); // "Original Constructor"
console.log(instance.constructor.name); // "Overridden Constructor"
console.log(instance.constructor.prototype === Object.getPrototypeOf(instance)); // "false"
This example is bogus. A special place in hell is reserved for people who reassign Constructor.prototype.constructor.
- Constructors have a
prototypeproperty which becomes their instances[[Prototype]]internal slot. - The
prototypeinitially has a single property,constructor, which points back to the original constructor function. - The
Constructor.prototype.constructoris useful to superclasses to create new instances ofthis's class.
For example, here's a Container class that is safe to extend and still call clone() on:
function Container(items) {
this.items = items;
}
Container.prototype.clone = function() {
// we rely on prototype.constructor not being overridden
return new (Object.getPrototypeOf(this).constructor)([...this.items]);
}
function UserContainer(users) {
Container.call(this, users);
}
Object.setPrototypeOf(UserContainer, Container);
Object.setPrototypeOf(UserContainer.prototype, Container.prototype);
UserContainer.prototype.logoutAll = function() { /** ... */ }
const users = new UserContainer([]);
const users2 = users.clone();
console.log(users2 instanceof UserContainer); // true
As far as I'm aware there's no good reason to ever change prototype.constructor, other than as a good April Fools joke.
UPDATE 2021-08-11
It turns out some people DO reassign or override the constructor property.
Take a look at this example from webpack's library for events/hooks/callbacks, Tapable.
// https://github.com/webpack/tapable/blob/v2.2.0/lib/SyncHook.js#L37
function SyncHook(args = [], name = undefined) {
const hook = new Hook(args, name);
hook.constructor = SyncHook;
hook.tapAsync = TAP_ASYNC;
hook.tapPromise = TAP_PROMISE;
hook.compile = COMPILE;
return hook;
}
Calling new SyncHook() returns an instance of Hook with a constructor property set to SyncHook. The new instances property, hook.constructor, overrides the inherited property, Hook.prototype.constructor, such that hook.constructor === SyncHook. However, hook instanceof SyncHook is false.
Just don't ask me why!
Further Reading
- Older libraries like
expressstill use prototypes and constructors. Check out Express.Request for an example. Express uses Object.create() to use blueprint objects,reqandres, as the[[Prototype]]s for thereqandresof a request instance.



Latest comments (4)
"Since ES6 appeared and the ability to add "classes" I see that it has only caused a confusion in the way of programming, making a javascript code as a migration from java." by dev.to/damxipo/functional-programm...
Thanks for this post, very complete and informative!
What is wrong with example 4?
constructor of
MyClassdoes not sit inMyClass.prototype. what a surprise that it does not get overriden...I noticed that you are saying it is a bogus example. I just felt that unlike the other examples where it might look surprising to some the No.4 is just plain error.
The confusing part is this:
Which makes sense once you know that
instance.constructoris accessing an alteredconstuctorproperty from the prototype, but otherwise might be quite confusing.You're right that it probably doesn't belong with the other examples. Hopefully JavaScript developers unaware of
prototype.constructormight find it interesting.