JavaScript’s Object-Oriented Identity Crisis
By: Matthew Macdonald
Tags: Programming, JavaScript, Oop, Object Oriented, Web Development

There’s no shortage of programming languages in the world. There are old-fashioned procedural languages (like COBOL), and classic object-oriented languages (like Java). There are functional languages — very on-trend right now — and multiparadigm languages like C#, which give you a certain amount of flexibility to choose.

And then there are confused languages, like JavaScript.

Too harsh? How else to describe a language that survived for 15 wildly successful years and then — a mere 5 years ago — abruptly added classes to the language?

I know what you’re thinking. Classes are just syntactic sugar. JavaScript is all about prototypes under the covers, and classes are just a bit of window dressing to satisfy the object-lovers. And that’s true, up to a point. But in practice, the introduction of classes gave us two overlapping models with uneven support. Were you in the habit of using closures to store private data in a constructor function pre-ES6? Then you might have a hard time with classes, which currently have no native support for private members. Used to creating property procedures with defineProperty() ? Well, now you have property procedures which neatly wrap some of that functionality, but not all of it. Want to create custom objects with inheritance hierarchies? Both models can do it, but neither one will satisfy you in the end.

In fact, few languages have as fractured a development model as JavaScript. Forget the wild proliferation of application-building frameworks (React, Vue, Ember, and so on). JavaScript developers have a hard time agreeing on canonical coding practices in even the simplest applications.

Let’s dig deeper.

Inheritance: All of the bad, none of the good

Plenty of electronic ink has been spilled about the evils of inheritance. Hot takes aside, it’s certainly true that there’s no faster way to complicate your object model and introduce unnecessary dependencies than to throw in a dash of inheritance. But inheritance can also standardize and formalize the behavior of complex frameworks that need objects to work together. So how does JavaScript do on this front?

In the old days of JavaScript, we could simulate inheritance with the prototype pattern, but it was ugly. Ever since ES6 classes, life has been simpler. Today, inheritance is just an extends keyword away. Check out this example is a parent Shape class and a Triangle , Circle , and Square that derive from it:

At first glance, this arrangement looks pretty neat. But in practical life, it’s not as helpful as you might expect. To understand why, let’s quickly remind ourselves of the two typical goals of inheritance:

To reuse the code in the base class

To standardize an interface (a set of methods and properties) defined in the base class

If you just want to reuse code, you’re almost always better using composition. In other words, don’t build an Employee object that derives from a Person object, build an Employee object that holds onto a PersonDetails objects. (Or just make bigger classes and resist the urge to break every similar detail down into a separate piece. Sometimes less reuse means more flexibility to deal with the future. But that’s a whole different topic.)

The special part of inheritance—the way it imposes some kind of formal structure on your class system—doesn’t really hold up in JavaScript. Consider the example with the shape classes. At first, it looks good. You can write spiffy code like this:

// Create an array of different shapes

const shapes = [new Triangle(15, 8), new Circle(8), new Square(7)] // Sort them by area from smallest to largest

// This works because every Shape-derived class support getArea()

shapes.sort( (a,b) => a.getArea()-b.getArea() ); console.log(shapes);

// Here's the new order: Square, Triangle, Circle

But here’s the problem — you don’t need inheritance to make this work. JavaScript is a loosely typed language, and you could call getArea() on Triangle and Circle and Square objects event if they didn’t share a parent class that defined the method. Yes, formalizing this interface with inheritance helps to make these requirements explicit. It’s also allows you to test objects using instanceof . But that’s a lot of extra weight for a second-rate attempt at self-documenting code.

And we haven’t addressed some of the uglier parts of the shape example. Like the fact that the Shape class doesn’t mean anything on its own, but we had to create it like a normal class anyway. (JavaScript doesn’t support abstract classes.) Similarly, the Shape.getArea() method has no meaning, but we had to write in some nonsense code there too. (JavaScript doesn’t support virtual methods.) We could have avoided the whole headache if we just defined an interface and implement that instead. (Nope, JavaScript doesn’t support interfaces either.)

Here’s the bottom line:

Classes in JavasScript are useful. But supporting inheritance without the other tools of classical object-oriented languages is a doomed enterprise.

Property procedures without encapsulation

Who doesn’t like property procedures? I still remember when it was the custom to create a boilerplate property getter and setter for every public detail (cough, C#). Why? Because you needed them to support other features (like data binding). And why not? It gave you an extra layer of indirection if you need to slip in some validation or change-tracking code later.

But in JavaScript, it’s a different story. We have property procedures, but experienced developers suggest we don’t use them. Don’t believe me? Both the Google JavaScript Style Guide and the near-canonical Airbnb JavaScript Style Guide — written by far smarter programmers than me — forbid the use of property procedures.

One of the reasons is that new properties are never more than a typo away. Even in strict mode, and even with a linter, you can make a silent but nasty mistake like this:

// This isn't the property you want (that's dateOfBirth)

// but JavaScript creates it anyway, and you won't notice

// the mistake until it's too late

person.DateOfBirth = new Date(2035, 10, 10);

On the other hand, if you’re using setXxx() and getXxx() methods, you’ll never experience the same pain:

// You can't call a function that doesn't exist, so this typo

// ("Data" instead of "Date") always fails and won't be ignored

person.setDataOfBirth(new Date(2035, 10, 10));

It’s bad enough that people sometimes call Object.seal() to harden their objects, so misspelled properties become actual runtime errors. (But don’t do this. Let’s use our built-in objects for the purposes that they’re designed.)

A more grating issue is the fact that JavaScript added properties without adding support for private fields. Technically, JavaScript doesn’t support any type of field. You’re just creating public instance properties on this (that’s the official nomenclature), same as you can with any JavaScript object. This arrangement means you need to “create” your properties in the constructor, which definitely isn’t my favorite design arrangement.

It also means that if you want use your property procedure for a useful purpose — like validation — you’ve got to trust the calling code not to mess with your data. You don’t just have a backdoor to changing the underlying public property — you’ve got a front door and it’s propped open all day long. Check it out in this Person class:

Here you’ve got three public instance properties, firstName , lastName , and _dateOfBirth , and one “virtual property”, dateOfBirth . When the constructor sets dateOfBirth , it’s not at all clear that you’re using the property setter. And if the calling code accidentally uses _dateOfBirth instead of dateOfBirth , your validation code is moot.

Name collisions are particularly common in this system. You’ve got to make sure to differentiate the names of your public instance property, your property setter, and your constructor parameter. The convention is that when we see a public instance property that’s named with a preceding underscore, we should pretend it’s private. (Hands off my data!) But while a convention is good for keeping a bare minimum level of sanity, a safety mechanism with real enforcement would be an order of magnitude better.

You could replace the dateOfBirth property with a setDateOfBirth() method, but even that won’t solve the privacy problem. Future versions of the JavaScript standard promise a fix, eventually. In the meantime, some people create the most creatively dangerous workarounds for private properties, like hiding them in nested methods in the constructor (awkward), using Symbol to create obfuscated property names (weird), or using a WeakMap to store the data (seriously?). Here’s a good rule of thumb: If you’re using a nonstandard workaround to implement a language feature in every class you write, you’re on very dubious ground.

I could go on to complain about other bits of property metadata that don’t make it into the class system, and are only settable procedurally through defineProperty() . But my real complaint is this:

If classes are meant to be encapsulated packages of data and functionality, JavaScript’s implementation doesn’t satisfy that description. It groups things together, but it doesn’t let you hide details out of sight. It violates the black box principle, which is as close to sacred as OOP gets.

Where are we?

I’m not in love with JavaScript. I certainly don’t dislike it. I do greatly admire it. Few languages have taken such a simple premise and made it so successful. And as someone who’s underestimated the language before, I definitely won’t repeat that mistake.

But if I want to build something ambitious in JavaScript today, I’m disturbed by the gap between pure procedural programming and “let’s pretend everything’s OK and use object-oriented practices all the way down” wishful thinking. The former approach is limited, the latter is naïve, and it seems that every developer team is left to improvise the right compromise somewhere in between.

Today, I’m more comfortable in the more rigorous, formalized world of TypeScript. I’ll trade free-wheeling power for safety any day. That said, there are interesting developments on the horizon for future version of JavaScript. Perhaps those will close the gap enough that I’ll be able to be productive on vanilla JS again, someday. Or maybe not. But until then, I’ll end with a confession from another wise coder: