Stop Using JavaScript Classes!

Johnny Araujo
9 min readMar 30, 2022

--

Are you mired in a JavaScript codebase full of classes? Are you new to JavaScript and tempted to reuse patterns from object oriented languages? Do you ever wonder if there’s a better way of doing things in JS?

Fear not! By learning to write idiomatic JavaScript utilizing modules instead of classes, you can write code that is less cumbersome, more readable, easier to maintain, and provides a more pleasant engineering experience overall. In this article, I’ll show you why classes should be avoided in idiomatic JavaScript, and why modules should be your go-to choice.

*Note: You should be somewhat familiar with object oriented programing (OOP) and JavaScript (JS) before proceeding.

The Problem:

Below is an example of the kind of JavaScript I often see in the wild:

class ContrivedClass {
constructor(db) {
this.db = db
}
getPerson() {
return this.db.getPerson();
}
}
// somewhere else in the code, this class is only called onceconst contrivedClass = new ContrivedClass(db);
const person = contrivedClass.getPerson();
// do something with person

Above you see a JavaScript class with only one method and one constructor argument for injecting a dependency, and the class is only ever instantiated once. And my reaction is always the same…

Why is this my reaction? Because in JavaScript, there exists a plethora of better ways to write code, and yet in some codebases, classes seem to dominate more than Michael Jordan in Space Jam! (Sorry, Lebron.)

You seem upset. What are classes and why do you have opinions about them?

To understand why classes should not be the preferred way of organizing JavaScript code, we must first understand what classes are and where they excel. According to Oracle’s Java documentation, in object-oriented-programming, an object “stores its state in fields (variables in some programming languages) and exposes its behavior through methods (functions in some programming languages). Methods operate on an object’s internal state and serve as the primary mechanism for object-to-object communication”. A class, then, is “the blueprint from which individual objects are created”.

The structure of the class typically includes instance fields, a constructor, and methods:

  • Instance fields hold values as local state for the instantiated class.
  • A constructor is a method that receives the arguments the class needs in order to be instantiated and utilizes those arguments to return the instance of the class (the “object”).
  • Methods are functions that often manipulate the instantiated class’s data. Methods typically come in 2 forms, public and private:
  • Public methods are functions that can be called outside of the instantiated class by other code that utilizes the object.
  • Private methods, as noted by their name, cannot be called outside of the instantiated class.
  • Private methods are utilized by public methods or even other private methods to accomplish tasks where the implementation details are not relevant to the code using the instantiated class.

Below is a simple example of how to utilize a class that highlights many aspects of classes that programmers find attractive. The class provides a clear distinction between what the code consuming the class needs to be aware of and what it doesn’t. The class has built-in mechanics for how you write code, store data, manipulate data, and expose data. It’s easily reproducible — just use the new keyword to create another instance of the class to your heart's content. It even has capabilities for more advanced techniques such as inheritance, which can limit the work required to do similar but distinct tasks, but also has its drawbacks.

class Book {
// declare the values you want to store as part of this class
author;
title;
readCount;
// declare how the instance fields will get initialized
constructor(author, title) {
this.author = author;
this.title = title;
this.readCount = 0;
}
// create a private method that abstracts an implementation detail for use by a public method
incrementReadCount() {
this.readCount += 1;
}
// create a public method that can be executed outside of the class
read() {
console.log('This is a good book!');
this.incrementReadCount();
}
getReadCount() {
return this.readCount;
}
}
// somewhere in a different code blockconst myFirstBook = new Book('Jack Beaton', 'Getting To Mars');
myFirstBook.getReadCount(); // 0
myFirstBook.read(); // 'This is a good book!'
myFirstBook.incrementReadCount(); // calling private methods outside the class won't work in strict OOP languages
myFirstBook.read(); // 'This is a good book!'
myFirstBook.readCount; // directly accessing a class's private fields won't work in strict OOP languages
myFirstBook.getReadCount(); // 2

Classes seem pretty cool! So what’s so bad about using them in JS?

There’s nothing inherently bad about using this pattern in JS. However, the problem I’ve frequently encountered is that this is sometimes the only pattern, or a frequently misused pattern, in a JS codebase. When deciding between what patterns to use, the team should pick the pattern that best addresses the problem being solved in the language being used. But if the OOP pattern of classes is the only way you know how to or like to code, then of course there will be times when using it will make the code harder for others to understand because the pattern is not a natural fit for the problem and/or programming language.

So what is an alternative to classes in JS?

JS modules, of course.

What’s a JS module?

In JavaScript, a module is a file that exports features, such as variables, objects, and functions, that can be used elsewhere. They are defined using the export syntax, and code within the module will have scope access to all variables declared in the file in which it was defined. The exported features can then be imported into any other file or module. But code outside of the module will not have access to code in the module that hasn’t been exported.

The variables you export can take any shape, including a class, as long as it’s a valid JS variable, function, or object. In practice, this means that in a given file, we might declare variables outside of a data structure but still use them within the structure. This is called a closure. In other languages this is not allowed, but JS has robust support for closures.

// some js file
const a = 1;
const b = 2;
export function add() {
return a + b;
}
// some other js file that imports the add() function
add(); // 3

So how might we code the Book data structure using a JS module instead of a class?

Like this:

// some js file// the exported function is what other modules will have access to
export function createBook(authorName, bookTitle) {
// the following variables and functions are declared within the scope of the createBook function so other book instances or code cannot access these variables
const author = authorName;
const title = bookTitle;
let readCount = 0;
function incrementReadCount() {
readCount += 1;
}
function read() {
console.log('This is a good book!');
incrementReadCount();
}
function getReadCount() {
return readCount;
}
// only the methods listed as key-value pairs can be accessed by the returned Object
return {
read,
getReadCount,
};
}
// in some other fileconst mySecondBook = createBook('Gabriel Rumbaut', 'Cats Are Better Than Dogs');
mySecondBook.getReadCount(); // 0
mySecondBook.read(); // 'This is a good book!'
mySecondBook.incrementReadCount(); // will throw an error
mySecondBook.read(); // 'This is a good book!'
mySecondBook.readCount; // will also throw an error
mySecondBook.getReadCount(); // 2

As you can see, all the things we love about classes are in our module, without having to use the class syntax. There’s less boilerplate as instance fields and methods are replaced with simple variables, and their status as public or private is denoted by their inclusion or exclusion in the return of the exported createBook function module. By exporting a function that explicitly returns an object, we are able to forego the new syntax completely.

But what about dependency injection?

It is true that many like using classes in JS because the constructor function allows for easy dependency injection, where we pass in dependencies to the new object rather than hard-coding and coupling them to the class. For example, if my class requires making calls to a database, I can configure the constructor to receive a DB instance. This is fine as well, but remember, JS natively allows for the importing of modules created elsewhere! So if you want access to that DB instance, export it as a module and import it in the module where you actually need it. You get all the benefits of dependency injection without classes!

// old: db created or imported here and passed into Class
const library = new Library(db);
// new: db created or imported here and passed into Module
const library = createLibrary(db);

Moreover, you might not need dependency injection. Unless you are going to construct different Library instances with different DB instances, importing the DB directly into the module hosting the Library class/function will save you from having to write redundant code across multiple files. If you don’t foreseeably need dependency injection, there’s no need to go out of your way to support it.

// alternate #1: db imported within the Class
const library = new Library();
// alternate #2: db imported into Module
const library = createLibrary();

If I can do everything using modules, why do I need classes?

In JavaScript, you don’t! You can write any program you want without utilizing classes or the this keyword ever! Indeed, the class syntax is somewhat new to JavaScript, and object oriented code was written with functions beforehand. The class syntax is just syntactic sugar over that function-based approach to OOP.

So why do I see classes so often?

Because prior to ES5, most people didn’t take JS seriously as a language. The inclusion of class syntax in JS was an intentional decision to attract these more experienced programmers and lower the barrier of entry to make JS more popular. It worked extremely well, but at a cost. Since most people learned JS after learning an OOP language like Java, C++, or even Python or Ruby, it was easier to just rewrite what they already knew using JS syntax. And when newbies wanted to learn JS from scratch, they had all this OOP JS code as real life examples for how to implement common structures, so they too are now OOP programmers.

I get the sense you don’t actually think anyone should use classes in JS.

What gave that away 😃? For reasons that Kyle Simpson lays out in much more detail than I ever could, classes are not first class citizens in JS in the same way that they are in other languages. Most don’t know this, but under the hood, all classes are functions, and all functions are objects. (Arrays are objects, too, but that’s besides the point.) As a result, you are better off in most cases just writing objects and functions directly as it is more behaviorally consistent, more pleasant to maintain, less cumbersome to write, and easier to read. That way when your peers look at your code, they aren’t feeling confused like Johnny from Schitts Creek.

So classes in JS are a choice, not a way of life. I get it. In your opinion then, when should I use them and when should I avoid them?

Consider using classes when:

  • Most of the team is unfamiliar with modules
  • The code you are writing is not expected to be an exemplar of idiomatic JS code
  • You want to leverage broadly known patterns that are strongly enforced in a class paradigm (e.g., local state, public/private, etc.)
  • You plan on instantiating a given class multiple times
  • You don’t always instantiate classes with the same arguments
  • You plan on leveraging all or most of a class’s capabilities (i.e., inheritance, dependency injection, etc.)

Consider avoiding classes when:

  • You only instantiate your class once in a given runtime
  • Your data structure does not require any local state
  • You have minimal public methods
  • You aren’t extending your data structure to create new ones
  • Your constructors are only used for dependency injection
  • Your constructors are always called with the same arguments
  • You want to avoid using this

But I like using classes in JS. Do you really think I should abandon them?

Yes! But more importantly, I just need you to know that you have other options. Not just in coding, but in life. If you challenge basic assumptions about the way you do things, you will discover and learn much more than if you continue to do things as you always have.

If you still feel strongly about using classes or are under constraints that prevent you from properly implementing modules, that’s fine. If anything, you are in luck because TC39 is adding public/private/static syntax into the next version of JS, ES2022. But please be sure to understand the implications of your decision, especially if the classes you write don’t utilize all the benefits that classes have to offer!

For more information on all the different types of patterns you can use for coding in JavaScript, please see Learning JavaScript Design Patterns by Addy Osmani.

--

--