Part 1: Prototypes vs. classes
In class-based object-oriented languages, a class is a type implementation blueprint. The terms “class” and “type” are often used interchangeably in that context, and while not technically accurate it is how many developers – and, indeed, languages – think about them. Suffice to say that a (concrete) class generally serves two purposes: it defines both an implementation of specific behavior and state, and an implicit interface through which that implementation can be accessed. More importantly, it imparts those things to all object instances created by its constructor(s), thereby providing and upholding certain guarantees about the shape and function of those objects.
A prototype, on the other hand, is not related to types at all, but is simply an object instance. In fact, this statement is so central to the whole subject under discussion that we’ll go ahead and repeat it in large type (pun intended):
A prototype is an object instance.
That object instance in turn contains shared properties which all other objects that have a link to it may utilize. This enables prototypes to do something that classes in statically typed languages cannot: they can be modified and even exchanged completely at runtime. This is how many browser polyfills work – for example, running this quick-and-dirty snippet in a pre-ES6 browser:
...will make the following calls work from that point on, since the object instance defining the prototype for all strings will have acquired a new callable property:
Let’s proceed to change the prototype of an already constructed object on the fly, thereby affecting which properties that object has access to:
Should we then go on to change the prototype of that new prototype in turn like the following, the frobnicate()
method would suddenly become available through our original instance again, without touching either foo
or BarProto
:
Reassignments and deletions of prototype properties are also perfectly possible, with similar effects:
Magic? No, just references between dynamic object instances in memory. As such, a prototypal relationship is not necessarily a parent-child relationship, but rather just objects linking to other objects, without an implied hierarchy. And that is what JavaScript is all about.
Here's a CodePen of the example above in full.
To inherit or not to inherit
Even though you will frequently encounter the term “prototypal inheritance”, it’s actually something of a misnomer. When speaking about classes, inheritance is typically central to the argument, where child classes are thought of as acquiring properties from their parents. In a prototype system, the situation is reversed: when an object is asked for a member it does not itself have, it delegates the request to its prototype which then acts on behalf of the original object. Should the prototype object not include the member either, that object’s prototype is consulted, and so on, forming what is known as the prototype chain.
There’s a quote that’s been repeated so often that it’s become a software engineering aphorism, or even axiom:
Favor object composition over class inheritance.
This exhortation does of course originate from the (in)famous Gang of Four’s book Design Patterns, but despite its widespread recognition, the actual observance of it is often curiously lacking in concrete projects and implementations. Let’s illustrate with a common form of example which I bet many will recognize (see the next article in the series for a more detailed account of the ES6 class syntax):
All perfectly well and good, with the expected prototype chains in place. But, before you know it, it has somehow grown absolutely imperative to also handle centaurs, which share characteristics from both Horse
and Human
, thereby not fitting into the neat hierarchy that has been running smoothly in production for a good while (and therefore cannot be changed without creating havoc).
Multiple inheritance vs. mixins
Some languages support multiple inheritance, which could solve this particular problem. Here’s a quick example in Python:
Anything that deals with multiple inheritance, however, must also deal with the diamond problem, and this type of technique really only fits when the desired object represents a straight combination of all its parents’ characteristics. For example, if our classes also were to include handling of the number of legs, we would have a clash that must be resolved in a satisfactory way so that the centaur can locomote properly.
But, since we’re favoring composition over inheritance (we are, aren’t we?), we can come up with a better way to handle the zoo using the concept of mixins, here presented in three different ways:
Here we’re utilizing the already defined classes for brevity, and then assigning additional functionality coming from independently defined mixin objects (termed exemplar prototypes, to contrast with the delegate prototypes we’ve looked at so far). The difference between the approaches, which can be seen in the console output, lies in where the mixed-in methods end up – in the object instance itself or in the prototype chain. Do note, however, that in all cases what’s actually stored is function references, so the instance approach incurs no overhead here since the functions themselves are not copied – in fact, it will be slightly more efficient since the mixed-in method calls don’t need to be delegated up the chain.
Here it should be noted that there are certain caveats to using Object.assign()
in this way depending on the nature of the exemplar prototype(s) involved, and you may have to resort to more verbose Object.defineProperty()
calls for a more robust mixin implementation, but for simple cases like this it is perfectly adequate. See MDN for more details.
You may also have seen something like this in the wild using class expressions, especially if you’re coming from TypeScript:
This also works, but as we can see it does so by extending the prototype chain rather than mixing in new properties in one place, so it’s actually a different technique. It also sets up extends
relationships which are decidedly not is-a and which also depend on the order of the function calls, thus actually ending up breaking the familiar class inheritance semantics which ES6 classes were meant to capitalize on. In addition, it’s less efficient since every step up the chain comes at a (small, but real) performance cost.
Here's another CodePen with all the mixin variants above.
Hierarchy vs. composition
Okay, so we’re still good (for certain values of “good”). Phew. However, in addition to the centaur requirement, it has also been decided that pigs do, indeed, fly – but they obviously don't lay eggs! And here our class hierarchy finally breaks down.
Fortunately, we can use composition to save the day once again. Here’s one of several possible ways of doing just that:
Here the “favor composition over inheritance” motto is realized in full: owls, kiwis and flying pigs all have the appropriate canFly
and fly()
members, whereas only pigs can wallow in the mud and only birds lay eggs – all without hierarchical relationships that are in danger of breaking or do not constitute an accurate representation of the actual state of affairs.
A similar effect can be achieved in C# or Java using default interface methods, but it comes with a caveat: in order to access the shared implementation, the instance variable must either be cast to the interface type, or a new composite interface containing all desired members must be created which can then be used to declare the type of the instance. E.g.:
The point is that (complex) class hierarchies tend to create lock-in effects, breaking out of which may either be impossible or introduce breaking changes in base classes, which affect each and every subclass from there on down – which, if you’re maintaining a public library of some kind, may have unexpected and unwanted consequences for userland extensions relying on your code. That sort of thing is, of course, precisely what principles such as SOLID are all about avoiding, but it is perfectly possible to paint yourself into corners anyway simply as a result of the tight coupling arising from the inheritance model (the circle-ellipse problem is a classic example). In other words, relying solely on inheritance forces you into trying to predict the future, which is rarely possible for anything but the simplest cases (and often not even then).
To construct or not to construct
So why does JavaScript have constructor functions and the new
operator at all, if it’s not based on classes? Brendan Eich, the creator of the language, has on numerous occasions stated that there was an edict from Netscape management at the time that their new browser scripting language must “look like Java”, and this is, quite likely, the reason why we have built-in things like new Date()
and new Array(10)
.
On the other hand, there are lots of examples from the wider JS community where factory functions are the default. jQuery’s main $
function should be familiar to most, and in the Node world things like const app = express();
or const server = http.createServer();
are commonplace. And let’s not forget native web APIs such as document.createElement()
or the dual nature of Error()
, which works the same both with new
(constructor) and without it (factory).
As always, there is more than one way of doing things in JavaScript.
Next up
In the next part of the series we will take a closer look at ES6 classes and how they actually work. Prepare to be surprised.
Continue to Part 2: ES6 classes aren't what you think they are