Part 3: Should class be considered harmful in JavaScript?

In the third and final part of the article series There are no classes in JavaScript we look at the consequences of introducing class semantics into a prototypal language, and how to handle them. Please read the previous parts first to get the full context.

To reiterate, ES6 classes are primarily syntactic sugar casting prototypal delegation (which is lesser known) in the guise of class inheritance (which is widely known). OK, but what’s the harm in offering this shortcut to those who feel more comfortable using it? Well, it depends. Possibly fairly little – but just as likely, quite a bit.

Conceptual mismatch

For one thing, since ES6 classes aren’t actually classes under the hood, developers who assume that they are may be in for some nasty surprises, especially when coming from a statically typed language. The “unbound this” problem outlined in part 2 is one thing, the fundamental misconception that you’re dealing with a type is another. This is precisely the sort of thing that is likely to lurk in the depths only to surface and bite your leg off when you least expect it.

But what I really wish to put the spotlight on is this: If you’re led into thinking in class-based terms and class-based terms only, you’re missing out on everything JavaScript has to offer which isn’t tied to class semantics and the bolted-on class syntax. In other words, you may be limiting your view to a single paradigm which isn’t even an accurate representation of the underlying mechanisms, thus also limiting your ability to take better advantage of the platform where you’re working. Most especially, the risk for (over)reliance on direct inheritance is high in cases where a compositional approach may be more suitable, even from people who swear by the GoF in all other respects.

Now, as we have seen it is often perfectly possible to “hack” those classes to break free of said limits, but just the fact that you’ve chosen a class in the first place may be enough for such possibilities never to occur to you – and if you hadn’t chosen a class those hacks would not be hacks at all, but rather just idiomatic JavaScript.

In the wild

But are those risks really something we need to take seriously? Can’t we trust developers to recognize them and mitigate them? Here I would answer “yes, absolutely” and “no, not necessarily”, respectively. Let’s illustrate.

Here at Cygni we use technical assignments as part of our recruitment process, and it is not uncommon that we see module files that look like this in JavaScript submissions:

Or even this:

A class like that, with no or constant internal state, is clearly functioning as simply a function library. Now, in languages like Java and C# you have no choice but to make it a class, but in JavaScript this has no business being a class at all and suggests that the author either comes from a class-based background and carries those idioms with them, or is fairly new to JavaScript and has been told that “classes are good”, but hasn’t given the matter much thought themselves. In either case, the result is quite unidiomatic JavaScript code, which is typically pointed out in reviews. If it’s a function library you’re writing, just use named exports and be done with it:

We have also seen people fall into the methods-as-event-handlers trap, where the value of this is unexpectedly (to them) overwritten. Since React was previously all (or nearly all) about classes, there is also no shortage of questions on the web about how and why component methods don’t work as expected when bound to JSX events, because the class definition simply doesn’t work like most people expect it to, and there are an almost equal number of articles about the pros and cons of the available mitigations. And this is despite the fact that the React documentation explicitly calls out this particular vulnerability – a vulnerability which only exists as a direct consequence of the ES6 class syntax for components, as it is not present when using createReactClass() or hooks (see below), or in plain functional components.

Wither now?

Despite all the caveats raised in the previous parts of this series – and as we have seen they are plentiful and far-reaching – the fact remains that there are now class-related semantics in the JavaScript (or, to be more precise, ECMAScript) specification. So which path will the language take in the future? The answer, it seems, is not that straightforward.

Official stance

On the one hand, much of the TC39 work being done nowadays, such as decorators and the previously touched-upon instance fields, focuses on class semantics over prototypes (or any other language construct, for that matter). Also, other web APIs such as Web Components are based on classes, with strongly coupled inheritance touted as the preferred extension mechanism. In other words, it seems as though the powers that be are determined to stay on the class track – some of them are even on record stating that requiring class syntax was the only way to get full buy-in on the Web Components spec, which from a technical standpoint is an artificially imposed limitation (check out hybrids if you’re as irked by it as many people in that issue thread).

Inofficial pushback

On the other hand, influential people in the JavaScript community like Douglas Crockford, Kyle Simpson, Eric Elliott and MPJ have argued against classes and/or constructors and new for many years, and a little search engine exercise will reveal that they are far (really far) from alone.

On the third hand (just go with it), several large frameworks have actually moved away from classes instead of toward them. As mentioned, React was previously very class-centric, with most component definitions extending the React.Component class and a strong prevalence of class syntax in the official documentation, but since version 16.8 a few years back the recommended way forward is to use hooks instead, which are not supported in class components at all. In fact, the hook docs now go as far as saying that “classes confuse both people and machines”.

Versions 1 and 2 of Vue used object literals in component definitions, but in the early stages of the development of version 3 the plan was to adopt classes instead, building on the existing class component plugin. However, the team ran into numerous problems with this approach and in the end decided to scrap it entirely in favor of functional composition, which is what ended up as the preferred alternative to object literals in the released product last autumn.

In other words, two of the three main frontend frameworks (sorry, Angular), which together encompass a significant portion of the larger JavaScript community, have come to the conclusion that the best way to “favor composition over inheritance” is not to use classes in the first place. Whatever you may think of them, or their motivations, that is something not easily dismissed.

Conclusion

As we have seen, JavaScript’s “classes” aren’t actually classes that you will find in class-based languages, and treating them as such may lead you into trouble you didn’t even know existed – or at the very least limit your expressiveness.

As previously mentioned, the introduction of class syntax in ES6 is typically sold as a way to ease developers more used to class-oriented programming into JavaScript. Due to its prevalence in formal education, the most common such transition language is likely to be Java, which is rather strict in the limitations it enforces, and it is even more likely that the developers in question have focused on inheritance as the primary tool for code reuse since that is what is usually given the most attention in OOP material, despite the GoF maxim. ES6 class syntax therefore presents a classic lose-lose situation for such developers, in direct opposition to the intention: not only does it misrepresent the underlying platform in a whole range of fundamental ways and open the door to some (to them) truly surprising behavior, it also hides or obscures a multitude of other patterns that the new language provides which go beyond the semantics that they’re familiar with. From that perspective, class is indeed harmful.

Now, as touched upon in the introduction, the general concept of a class does, of course, exist in other language flavors and paradigms as well, where some of the subconcepts under discussion don’t hold either. A good example is Python, which is also widely used in academia, and which as has been demonstrated behaves similarly to JavaScript in many (but not all!) ways due to its dynamic nature – the getBigValue() example from part 2 would work exactly the same, for instance. And, if you’re coming from PHP you will probably recognize traits as a version of the mixin pattern (or vice versa). And so on.

Regardless, everything boils down to one simple fact: if you don’t know how prototypes and closures work, you don’t know JavaScript. My view is that as developers we all have a responsibility to know the tools of our trade, and simply glossing over the very foundation of one of the most important and widespread programming languages today, whatever you may think of it, just won’t cut it.

What to do

If that sounds harsh, consider the upside: the better you know your tools, the wider the range of problems you can solve with them, and the smaller the collection of traps you’re likely to walk into. The point of this article is not to forbid the use of class in JavaScript outright, or to mandate either delegate or exemplar prototypes in all cases, or to say that inheritance should always be avoided. It is simply this: If and when you do encounter ES6 classes, in whatever circumstances, you ought to know what they actually are, and more importantly what they are not – and most importantly, what other constructs, mechanisms and techniques the language places at your disposal, as a complement or an alternative.

Furthermore, due to the highly disparate nature of the JS ecosystem, you are pretty much guaranteed to run into prototypes and/or other non-class techniques in some project or other sooner or later, regardless of what you normally work on – and then you’d better know what to do with them. ES6 classes have been in the language since 2015, prototypes and closures since 1995. Better catch up!

Mimicking classical inheritance is just one way in which the prototype system can be used, and a subpar one at that – it is not the only way. In fact, you don’t have to use it at all, and instead opt for end-to-end object composition and/or pure functional programming. Don't treat everything as nails just because you like your hammer – have a look around in the toolbox first.

To wit, statements to the effect that JavaScript isn’t object-oriented, or that it only became object-oriented with the introduction of ES6, are flat out false. In fact, it is all about objects – it just has never been class-oriented, and still isn’t. What it is is a very flexible and dynamic language with immense expressive power, and how to use that power is up to you – almost everything is on the table. But, as always, with great power comes you know what, and it is therefore of great import to learn to use the language on its own merits, not those of something (or someone) else which simply do not apply to it.

So, let’s prove Crockford wrong and finally start understanding JavaScript for what it actually is, shall we?