How ES6 classes really work and how to build your own
The 6th edition of ECMAScript (or ES6 for short) revolutionized the language, adding many new features, including classes and class based inheritance. The new syntax is easy to use without understanding the details and mostly does what you’d expect, but if you’re like me, that isn’t quite satisfying. How does the seemingly magical syntax actually work under the hood? How does it interact with other features in the language? Is it possible to emulate classes without using the class syntax? Here, I will answer these questions in gratuitous detail.
When you execute the code
foo[bar] , it converts
bar to a string if it isn’t already a string or symbol, then looks up that key among
foo’s properties and returns the value of the corresponding property (or calls its getter function as applicable). For literal string keys that are valid identifiers, there is the shorthand syntax
foo.bar which is equivalent to
foo["bar"] . So far, so simple.
[[Prototype]] slot which can be read and written using
Object.setPrototypeOf() respectively. By convention, internal slots and methods are written in [[double square brackets]] to distinguish them from ordinary properties.
Old style classes
this come from? Where did
prototype come from? What does
[[Construct]] . Any object with a
[[Call]] method is called a function, and any function that additionally has a
[[Construct]] method is called a constructor¹. The
[[Call]] method determines what happens when you invoke an object as a function, e.g.
foo(args) , while
[[Construct]] determines what happens when you invoke it as a new expression, i.e.
new foo or
new foo(args) .
For ordinary function definitions², calling
[[Construct]] will implicitly create a new object whose
[[Prototype]] is the
prototype property of the constructor function if that property exists and is object valued, or
Object.prototype otherwise. The newly created object is bound to the
this value inside the function’s local environment. If the function returns an object, the
new expression will evaluate to that object, otherwise, the
new expression evaluates to the implicitly created
As for the
prototype property, that is implicitly created whenever you define an ordinary function. Each newly defined function has a property named “prototype” defined upon it with a newly created object as its value. That object in turn has a
constructor property which points back to the original function. Note that this prototype property is not the same as the
[[Prototype]] slot. In the previous code example,
Foo is still just a function, so its
[[Prototype]] is the predefined object
Here is a diagram to illustrate the previous code sample with
[[Prototype]] relationships in black and property relationships in green and blue.
 You could conceivably have objects with a
[[Construct]] method and no
[[Call]] method, but the ECMAScript specification does not define any such objects. Therefore, all constructors are also functions.
 By ordinary function definitions, I mean functions defined using the regular
function keyword and nothing else, rather than
=> functions, generator functions, async functions, methods, etc. Of course, prior to ES6, this was the only kind of function definition.
New style classes
With that background out of the way, it’s time to examine ES6 class syntax. The previous code sample translates directly to the new syntax as follows:
As before, each class consists of a constructor function and a prototype object which refer to each other via the
constructor properties. However, the order of definition of the two is reversed. With an old style class, you define the constructor function, and the prototype object is created for you. With a new style class, the body of the class definition becomes the contents of the prototype object (except for static methods), and among them, you define a
constructor. The end result is the same either way.
So if the ES6 class syntax is just sugar for old style “classes”, what’s the point? Apart from looking a lot nicer and adding safety checks, the new class syntax also has functionality that was impossible pre-ES6, specifically, class based inheritance. When you define a class with the new syntax, you can optionally provide a super class for the class to inherit from as demonstrated below:
This example by itself can still be emulated without class syntax, although the code required is a lot uglier.
With class based inheritance, the rule is simple — each part of the pair has as its prototype the corresponding part of the superclass. So the constructor of the superclass is the
[[Prototype]] of the subclass constructor and the prototype object of the superclass is the
[[Prototype]] of the subclass prototype object. Here’s a diagram to illustrate (only the
[[Prototypes]] are shown; properties are omitted for clarity).
There is no direct and convenient way to set up these
[[Prototype]] relationships without using class syntax, but you can set them manually using
Object.setPrototypeOf(), introduced in ES5.
However, the example above notably avoids doing anything in the constructors. In particular, it avoids
super, a new piece of syntax that allows subclasses to access the properties and constructor of the superclass. This is much more complicated, and is in fact impossible to fully emulate in ES5, although it can be emulated in ES6 without using class syntax or super through use of
Superclass property access
There are two uses for
super — calling a superclass constructor, or accessing properties of the superclass. The second case is simpler, so we’ll cover it first.
The way that
super works is that each function has an internal slot called
[[HomeObject]] , which holds the object that the function was originally defined within if it was originally defined as a method. For a class definition, this object will be the prototype object of the class, i.e.
Foo.prototype . When you access a property via
super["foo"], it is equivalent to
With this understanding of how
super works behind the scenes, you can predict how it will behave even under complicated and unusual circumstances. For example, a function’s
[[HomeObject]] is fixed at definition time and will not change even if you later assign the function to other objects as shown below.
In the above example, we took a function originally defined in
D.prototype and copied it over to
B.prototype. Since the
[[HomeObject]] still points to
super access looks in the
D.prototype, which is
C.prototype. The result is that
C's copy of foo is called even though
C is nowhere in
b's prototype chain.
Likewise, the fact that
[[HomeObject]].[[Prototype]] is looked up on every evaluation of the
super expression means that it will see changes to the
[[Prototype]] and return new results, as shown below.
As a side note,
super is not limited to class definitions. It can also be used from any function defined within an object literal using the new method shorthand syntax, in which case
[[HomeObject]] will be the enclosing object literal. Of course, the
[[Prototype]] of an object literal will always be
Object.prototype so this isn’t terribly useful unless you manually reassign the prototype as is done below.
Emulating super properties
There is no way to manually set
[[HomeObject]] on our methods, but we can emulate it by just saving the value and doing the resolution manually as shown below. It’s not as convenient as just writing
super, but at least it works.
Note that we need to use
.call(this) to ensure that the super method gets called with the right
this value. If the method has a property which shadows
Function.prototype.call for some reason, we could instead use
Function.prototype.call.call(foo, this) or
Reflect.apply(foo, this) , which are more reliable but verbose.
Super in static methods
You can also use
super from static methods. Static methods are the same as regular methods, except that they are defined as properties on the constructor function instead of on the prototype object.
super can be emulated within static methods in the same was as with normal methods. The only difference is that
[[HomeObject]] is now the constructor function rather than the prototype object.
[[Construct]] method of an ordinary constructor function is invoked, a new object is implicitly created and bound to the
this value inside the function. However, subclass constructors follow different rules. There is no automatically created
this value and attempting to access
this results in an error. Instead, you must call the constructor of the superclass via
super(args) . The result of the superclass constructor is then bound to the local
this value, after which you can access it in the subclass constructor as normal.
This of course presents problems if you want to create an old style class that can properly interoperate with new style classes. There is no problem when subclassing an old style class with a new style class, since the base class constructor is just an ordinary constructor function either way. However, subclassing a new style class with an old style class will not work properly, since old style constructors are always base constructors and don’t have the special subclass constructor behavior.
To make the challenge concrete, suppose we have a new style class
Base whose definition is unknown and cannot be changed, and we wish to subclass it without using class syntax, while remaining compatible with whatever code in
Base is expecting a true subclass.
First off, we will assume that
Base is not using proxies, or nondeterministic computed properties, or anything else weird like that, since our solution will likely access the properties of
Base a different number of times or in a different order than a real subclass would, and there’s nothing we can do about that.
After that, the question becomes how to set up the constructor call chain. As with regular
super properties, we can easily get the superclass constructor using
Object.getPrototypeOf(homeObject).constructor. But how to invoke it? Luckily, we can use
Reflect.construct() to manually invoke the internal
[[Construct]] method of any constructor function.
There’s no way to emulate the special behavior of the
this binding, but we can just ignore
this and use a local variable to store the “real”
this value, named
$this in the example below.
return $this; line above. Recall that if a constructor function returns an object, that object will be used as the value of the
new expression instead of the implicitly created
So, mission accomplished? Not quite. The
obj value in the above example is not actually an instance of
Child, i.e. it does not have
Child.prototype in its prototype chain. This is because
Base‘s constructor didn’t know anything about
Child and hence returned an object that was just a plain instance of
So how is this problem solved for real classes?
[[Construct]], and by extension,
Reflect.construct, actually take three parameters. The third parameter,
newTarget, is a reference to the constructor that was originally invoked in the
new expression, and hence the constructor of the bottom-most (most derived) class in the inheritance hierarchy. Once control flow reaches the constructor of the base class, the implicitly created
this object will have
newTarget as its
Therefore, we can make
Base construct an instance of
Child by invoking the constructor via
Reflect.construct(constructor, args, Child). However, this still isn’t quite right, because it will break whenever someone else subclasses
Child. Instead of hardcoding the child class, we need to pass through
newTarget unchanged. Luckily, it can be accessed within constructors using the special
new.target syntax. This leads to the final solution below:
This covers all the major functionality of classes, but there are a few other minor differences, mostly safety checks added to the new class syntax. For example, the
prototype property automatically added to function definitions is writeable by default, but the
prototype property of class constructors is not writeable. We can easily make ours non-writeable as well by calling
Object.defineProperty() . Alternatively, you could just call
Object.freeze() if you want the whole thing to be immutable.
Another new protection is that class constructors will throw a TypeError if you try to
[[Call]] them instead of constructing them with
new. Our constructor above happens to throw a TypeError as well, but only indirectly, because
undefined when the function is
Reflect.construct() throws a TypeError if you explicitly pass
undefined as the last argument. Since the TypeError is coincidental here, the resulting error message is rather confusing. It might be useful to add an explicit check for
new.target that throws an error with a more helpful error message.
Anyway, I hope you enjoyed this post and learned as much as I did in the process of researching it. The above techniques are rarely useful in real world code, but it is still important to understand how things work under the hood in case you have an unusual use case which requires reaching for the black magic, or more likely, you’re stuck having to debug somebody else’s black magic.