We don't need classes

Sugar Oh, honey, honey

A few years back, JavaScript added a long-awaited feature to the language: Classes. So naturally, developers from class-heavy languages were happy to find their old friend in JavaScript, even if that old friend behaves mainly as syntax sugar. So first, let’s look at how classes work in JavaScript, and then we can tackle why we don’t need them.

Let’s start by writing the classic Shape class:

class Shape {
	constructor({ name = "shape", x, y }) {
		Object.assign(this, { name, x, y });
	}
	move({ x, y }) {
		Object.assign(this, { x, y });
	}
}

class Circle extends Shape {
	constructor({ name = "circle", x, y, radius }) {
		super({ name, x, y });
		Object.assign(this, { radius });
	}
}

const circle = new Circle({ x: 10, y: 10, radius: 5 });
circle.move({ x: 20, y: 20 });

In practice, that is mainly syntax sugar for functions:

function Shape({ name = "shape", x, y }) {
	Object.assign(this, { name, x, y });
}

Shape.prototype.move = function ({ x, y }) {
	Object.assign(this, { x, y });
};

function Circle({ name = "circle", x, y, radius }) {
	Shape.call(this, { name, x, y });
	Object.assign(this, { radius });
}

Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle;

const circle = new Circle({ x: 10, y: 10, radius: 5 });
circle.move({ x: 20, y: 20 });

The main takeaway is that when we use JavaScript classes, we write functions with “unnecessary complexity.” This complexity applies to the class and all its instances (this binding, new operator, static and private fields, etc.). The problem with said complexity is that it has no real benefit compared to just using functions.

Clean instead of classy

Instead of thinking about everything as a “class,” we should think about it as “data” and “calculations.” This way, we can write the previous example like this:

const createShape = ({ name = "shape", x, y }) => ({ name, x, y });

const update = properties => object => ({ ...object, ...properties });

const createCircle = ({ radius, ...props }) => ({
	...createShape({ name: "circle", ...props }),
	radius,
});

const circle = createCircle({ x: 10, y: 10, radius: 5 });
const movedCircle = update({ x: 20, y: 20 })(circle);

The main difference between this approach and the class-based is that this one avoids mutations, but besides that, we have a few advantages over “classes”:

  1. We don’t have to think about this, because we don’t use it at all.
  2. We don’t need to use new, so we can use it anywhere a function can be used, for example, callbacks.
  3. This approach discourages mutations, which makes our DX far better (predictable data, easier testing and debugging, etc.).

Common arguments in favor of classes

Some common arguments in favor of classes are:

“Classes help with code organization.”

One other thing that helps with code organization are modules. Instead of having a gigantic class with lots of methods, we can simply have a folder with files, each with a small function that we can use independently or group with all the others. So we get the same benefits of a class minus the limitations.

“Classes are more readable.”

Readability has nothing to do with classes and is more with proper naming and documentation. new Shape() and createShape() are pretty much the same. The main difference is that when we use the functional approach, the function can be passed directly as a callback, while the class needs to be put inside a function to use new or needs to have static methods.

“Classes are a better abstraction mechanism.”

I beg to differ. Functions are the most powerful abstraction mechanism in languages with first-class functions, such as JavaScript. Functions (especially fat arrow functions) require less code and are more flexible than classes. And I don’t want to repeat myself, but in practice, classes are still just functions.

“Without classes we loose identity.”

For starters, code that depends on instanceof to work is not good. Functions should work with any object that has the expected properties. Still, if we want to use instanceof, we can simply use Symbol.hasInstance property, no need for classes.

“Classes are required for OOP.”

This is a widespread misconception. OOP is just a paradigm, and classes aren’t part of it. According to Alan Kay, the “father of OOP”:

“OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things.” source

So we only need:

  • Messages.
  • Encapsulation.
  • Dynamic binding.

No mention of classes, and no need for them either. We get all that with functions, closures, and the module system.

Do we need classes?

Now think about it for a solid minute: Do we need classes, or are we just used to them? Before working as a Web developer, I was a fan of C++, so naturally, I loved classes, but as time passed, I realized that every problem I solved with a class had a cleaner and more straightforward solution just using functions.

We should look at our code in the places we used classes, and then think about how we’ll do that using functions instead. But, of course, the functional approach is always better.