Again, don’t take my word for it – let your browser show you. Here are three different ways of constructing an object:
As we can see, the structure of the instantiated objects using the class definition and the constructor function is exactly the same, with a prototype chain providing shared functionality. The third example uses a factory function with direct prototype assignment without the
new keyword and therefore lacks an actual constructor, but the generated object otherwise looks and works the same as the other two.
Should you need further evidence, proceed to run this:
Here the non-type nature of ES6 classes is driven home, clear as day: the underlying type of the constructed objects is simply
"object", and a class is considered to be a
"function", just as regular constructor functions (or factory functions, for that matter). In other words, an ES6 class isn’t actually a class like you would find in a statically typed class-based language, it just looks that way from a distance. And this is before even taking ES5 transpilation issues into account, which have risen to bite large actors such as babel and TypeScript in the past.
this is discarded entirely and that object takes the place of the constructed instance instead. Since ES6 classes are mostly just constructor functions in disguise and don’t define nominal types, their constructors exhibit the same behavior, which is really weird compared to class-based languages:
Further on the type track, it is fairly common to employ branching logic based on type checks in class-based languages, such as in these (completely made up and contrived) examples:
instanceof operator, but it doesn’t work like you might expect it to in a strongly typed language. Since prototypes are object instances and not types, what it actually does is check whether a reference to the object instance referenced by the
prototype property of the right-hand side object of the expression (which must be a function) occurs anywhere in the prototype chain of the left-hand side object. For simple cases, this produces familiar semantics:
However, things can quickly start to get more complicated:
Whoa, how did that happen? It’s actually not that difficult to understand, but it is far from obvious. First we have two completely independent constructors:
Bar, so naturally an object constructed from one of them is not considered an instance of the other. Then we set
Foo’s prototype property to that of
Bar, i.e. they now point to the same object instance. After that, when we ask
instanceof to check the prototype chain of the existing
Foo instance, it will no longer find a reference to the original
Foo prototype there, but when checking the chain of the
Bar instance it will find a reference to
Foo’s current prototype instance since that instance is now the same instance as
Bar’s prototype. Also note that this happened without modifying
Bar in any way. Didn’t expect that, did you? Most people wouldn’t.
Another gotcha is passing data between different execution contexts, such as windows or (i)frames. Anything created within one context is unique to that context, including built-in objects and their prototypes. Since prototypes are object instances, this means that the prototype chains of equivalent objects created by the same code in different contexts will also be different, and an
instanceof check between such objects will fail. Consider the following example (no embed, sorry):
When the button in the enclosing page is clicked, the four objects sent will arrive as expected to the
doStuff() function and the branching logic will work just fine. When the button in the iframe is clicked, the four objects sent from that frame to the enclosing page will also arrive perfectly intact, which the console logging demonstrates, but there the type checks will all fail – including those for the built-in
Error – since the object instances in their prototype chains will be different from the instances compared against, even though they originate from the exact same code. Oops.
In fact, it gets worse. Later editions of the language allow objects to specify a special method which can override the default prototype-instance logic of
instanceof. Let’s go wild and do something like this:
Or, the last two in Jest land:
Another somewhat contrived example for sure, but if you’re using someone else’s code you can’t be sure that they haven’t done something similar – and if the method has been defined as non-configurable, you can’t patch it yourself either. A typical use case would be formalized duck typing, as the final lines with the extension of
In short, relying on
this keyword, which deserves a whole article series on its own. Suffice to say, in a class context many would assume that
this will always refer to the current object instance and by and large this assumption holds in ES6 classes as well, realized using prototype chain delegation.
However, there are situations where it doesn’t hold, some of which you’re virtually guaranteed to encounter sooner or later. One has to do with event handlers, which are ubiquitous in a browser environment:
The first call to the
brag() method here outputs
"Our family has 3 parents and 12 children!" as you would expect. Clicking the button with the id
brag-button will, however, output
"Our family has undefined parents and [object HTMLCollection] children!" instead. Oops!?
The reason is that event handlers are called with the value of
this set to the object which triggered the event, which in this case will be a DOM node instance – and it just so happens that such instances lack a
parents property, but do in fact have a
children property that is itself an instance of
Family isn’t actually a class, there’s nothing preventing its methods from having their
this value modified just as with any other JS function call.
In fact, let’s do that explicitly:
In this example,
brag() will be set to the object literal passed to the
call() method, which is itself defined on the
This kind of thing was (and still is) used extensively to handle objects which share certain characteristics with built-in data structures, such as the array-like
arguments object available within functions which contains all the arguments said function was called with, regardless of formal parameters specified in the function definition:
arguments does not have
Array.prototype in its own prototype chain, trying to call
arguments.join() directly would fail with a
TypeError, but it is sufficiently similar to an actual
Array instance that we are able to borrow a method from the
Array prototype and let that operate on it instead.
This technique is also the only truly safe way of checking whether an arbitrary object (which is not
undefined) includes an arbitrary property on its own instance rather than somewhere in its prototype chain, since that chain might not reach the default top-level
This sort of thing is possible in Python as well, where duck typing is prevalent:
But in C#, which is strongly typed, it will only work with objects of the same nominal type, even when they share a shape:
...you will get a
TypeError saying that
this.parents can’t be accessed since
this is undefined. The reason is simple: by decoupling the function reference from the object instance, the function will just act as any other plain function when called, where (in strict mode)
this is indeed undefined.
With this setup all the
frobnicate() calls will have the same result, since arrow functions capture the original value of
this – like a lexical
Function.prototype.bind(), if you will. However, it is important to note that the function is now precisely what is mentioned above: an instance field, rather than a method defined by the class. Here’s the console representation of the previously created
As we can see, the
brag() method is defined on the
Family prototype, whereas the
frobnicate() method is defined on the
foo instance and will be recreated in full each time a new
Also, do note that the “unbound
this" problem is not in any way unique to classes, and that the event handler would be just as broken if it were defined in the same way in, for example, an explicit prototype object. The point is that with the
class syntax the use of
this is both expected and mandated, whereas in other contexts those restrictions do not necessarily apply, and the fact that
this isn’t automatically bound in ES6 classes is likely to come as a surprise to many.
A central tenet of classical object-orientation is the concept of encapsulation, namely that an object should keep its internal state hidden from the outside world and only expose a limited set of defined access points through which this state can be accessed and/or manipulated.
In other words, if you expect to be able to reliably control member visibility in ES6 classes, you’re out of luck at the moment. Using constructor functions or factory functions together with closures, however, it’s a breeze, provided that we’re willing to accept recreated instance methods as per the instance field example in the previous section:
Overloading and polymorphism
Here the overload resolution dispatches the
Frobnicate() call to
Foo's definition when it lacks arguments and to that of
Bar when it has one, since to the compiler they are different methods altogether. However, the final line will not compile at all since there is no method reachable from the interface defined by
Foo that takes an argument. If the signature in
Bar were to be changed so that the
quux parameter becomes optional, the third call would be dispatched to that method instead since it provides a better (nearer) match than the inherited method from
Under the same scenario, the first two calls work the same, but since Python doesn’t have overloading none of the last two will execute. The reason is that the
frobnicate() method from
Bar has hidden the inherited method from
Foo, so the actual dispatch will always target the former when going through the
bar instance, and this method requires a positional argument. If that parameter were made optional, the third call would work just as with the same modification in the C# case.
"undefined" in the output).
The other is prototype chain delegation, which compared to class hierarchy overload resolution is very simple: if the method isn’t found on the original object, each link in the prototype chain is checked in turn until a match is found, or the end of the chain is reached. Since JS function resolution does not depend on signatures as per above, all calls to
bar.frobnicate() are therefore dispatched to the method defined in
Bar and the parameter in the call in the last line is silently discarded.
Another consequence of this is that it is very easy to write “breaking overrides”, intentionally or not. Consider this (simplified) example:
getValue() that ends up getting called from
getBigValue() in the base class – or indeed that it is callable at all. Both
bar delegate the
getBigValue() call to
Foo.prototype, but in the latter case the subsequent call to
getValue() is handled by the definition in
Bar.prototype, since that member is reached first when the JS engine travels up the prototype chain from the instance referenced by
this – and we end up with an uncaught exception since number objects do not have a
toUpperCase() method anywhere in their prototype chain. The equivalent code in C# would generate a warning that
Bar’s version of
getValue() hides an inherited member, and the call in
getBigValue() would be resolved to
Foo’s version in both cases because of its signature.
And, of course, there is no way to mark anything – members or classes – abstract either, in order to force subclass implementation. In other words, the “usual” inheritance and overloading rules of classes in strongly-typed languages simply do not apply to members in ES6 class definitions. The list of expected things that “simply do not apply” is starting to get rather long, isn’t it?
In the third and final part of the series we will look at the consequences of all the things we have seen in the first two parts, and how we as developers should handle them.