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 once
const 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 block
const 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 file
const 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.
Top comments (27)
Ummm. I don't see why you are trying to avoid classes, they are useful and as you mentioned, soon they will be getting more powerful. And you haven't replaced classes with modules, you replaced them with closures. And after removing the comments and blank lines, both implementations of your book class/closure took the same number of lines.
Seriously, your point flew over my head. You are replacing classes with something less flexible, with no gains at all. Modules are nice, and they can be used to wrap classes just like they wrap closures. So, the module point is out of the window.
And about being "more behaviorally consistent", "more pleasant to maintain", "less cumbersome to write", and "easier to read". Really? In what way? Shouldn't you have explained these before concluding them?
Instead of telling people not to use classes, this would have been a nice article about closures and how they can be used for those who have class-phobia.
Thanks for reading! To be honest I care less about how people code and more that they understand the language that they are coding in. I tried to bring that point home in the end so sorry if that didn't come across. I'll do better next time ;)
I use functions over classes and I like it. I dont see the need to write a class in nodejs for my use cases.
To use a method in a class you need to import the entire class in the file you are wanting to use the method... modules allow you to pick and import specific functionality.
I like the fact that classes scope functions
product.get()
orproduct.create()
- but I use good naming conventions on my functionsproductGetAll()
andproductCreate()
and my IDE loves that because when i start typing 'prod' it gives me a list of all available methods for products - and this works for my db queries, router, utils etc.For me its about developer experience and patterns to make coding in my projects easy. I just dont see how classes would benefit me with the code i write - but that doesnt mean i avoid them, i just use what I feel is best for my projects.
Hey! Nice work with the Article, but I think the code focus way to much on high level implications rather than low-level ones. Meaning the implications of using Classes and how they really relate with JS prior ES6.
Overall, Classes came to allow more people easily start using the concept of Object without fully needing to understand/learn Prototype Oriented Programming which is what JS implements under the hood. Along with that, Classes does pretty more couple of things that easies the need for dealing with
this
and for instances Classes and Instances as well.The Article quite implies that modules are better than Classes which is not the case, and they shouldn’t be compared as are totally different things. A more real comparison is to construct Objects out of Functions and export them, or use Prototype for attaching methods and more.
Again, will be better to compare it to other things instead of directly against modules :)
Hi Charlie, thanks for reading! That focus was intentional because users such as yourself have a better low level grasp and aren't as quick to use these patterns. In my honest opinion if you know what you're doing, you can do whatever want, but many people have limited exposure and tend to run with whatever they've seen.
Regarding classes vs functions with methods, that's exactly how JS classes work under the hood so it wasn't an alternative I was interested in exploring. Good point though!
Hi Johnny :)
Sure you’re right on your first point, for new adopters it can be easy pick this approach mostly if your coming from a OOP language background like Java or C# (which are the most common), and I also do agree that as long as you’re confortable with what you’re doing it, should not be a problem at all :)
Sure! I felt it like more natural to compare as even that the JS Modules kinda can work like this, sometimes new adopters can confused them with what the Classes are really trying to achieve in JS. Thanks a lot for the feedback! Keep it going 🙌
I actually think it's more confusing having two paradigms. I sometimes open Javascript files that are written as classes and it takes me a little it of time to understand exactly what they were trying to accomplish since I'm used to working differently in JS. I'm sure it's the same if a OOP programmer opens a prototype/object based Javascript file.
Anyway, I'm not even sure classes in Javascript really give OOP programmers all the flexibility that classes in other languages do. Fortunately that's changing with the inclusion of things like decorators. Since classes in Javascript are purely syntactic sugar, I also wonder what extra you are getting? It's not like there's extra functionality that comes with classes.
Hi Johnny! Thanks for reading! It's definitely best if teams discuss beforehand which patterns to use and which to avoid. Consistency I find is usually more important than the patterns you end up sticking with.
Well, object oriented is a pretty strong and established programming paradigm. And it was introduced mainly for code organization! And object oriented is not about classes. It's about encapsulation, polymorphism and inheritance. The alternative that the author is suggesting is STILL OBJECT ORIENTED! Just a different syntax. If he was suggesting a different paradigm, like function or data oriented, maybe the article would have been more useful.
The reason why the article is getting heated comments is because the author is asking for them. You can clearly see that from the graphics used in his article. Besides, the use of strong language, like "Don't use classes" gives you the feeling that you are going to behold a great change. In a field where there's no absolute right and wrong, you expect someone telling you not to do something to have a pretty strong argument. Honestly, I feel like I was click-baited.
Classes in Javascript are very basic. The real benefit is DI when using decorators with Typescript. In one of my previous work I saw that if there's no clear structure in place it becomes a mess and over time I feel now that JS development suffers from a common structure, conventions. At scale the benefit of DI becomes useful but then when you reach that stage then you already have a mess that is too big to tackle and not worth it business wise. Developers create low quality code as result and best practices are an ideal in theory. Functions with 600 lines were looked over like it's fine and the company was just 5 years old. We really need to use something like Nest.js.
Nest.js is one of the few exceptions I make when it comes to using classes in Node. Thanks for reading!
DI becomes useful immediately with 0 lines of code. The mess will slow you down in minutes, not years. There is no excuse not to use best practices such as TDD and applying design patterns even for personal projects.
You definitely can replace classes with modules in the same way as you can just use static classes/methods in Java/C#/etc instead of object instances, but...
DI allows you to achieve loose coupling in your codebase and convenient mocking of different subsystems in your project's unit tests.
This very helps if you're working on a relatively large monolith project.
If it's the right tool for the job, by all means. Thanks for reading!
I've gone back and forth with classes, however I absolutely love typescript and we use it exclusively and as such I've come to appreciate classes more especially when paired with MobX. Mobx can automatically bind class methods and I basically never have to worry about "this context" being hijacked, it's just not a thing. And even if you want to create a method on a class that isn't a mobx action or flow, you can still enumerate all the functions in a class and automatically bind them all.
And with Stage 3 decorators coming classes are about to get really amazing. And writing little functions like "createBook" that returns it's methods/getters etc is really troublesome.
In typescript I can do this
Vanilla js, yeah classes are kind of meh, you can create classes via functions, but it's cumbersome to do that in Typescript. Typescript strongly prefers using classes when a class like thing is deseriable.
Very good
Thanks for reading!!
I didn't want to use classes in my latest project for some of the reasons stated here, but they're actually much faster in some use cases (at least from my tests creating thousands of instances).
If you want the following features: inheritance/extending, overriding getters/setters on sub classes (key word "overriding"), then you can't beat classes in javascript.
I've toyed around for ages with factory function+mixins, and prototypes. I don't know what's going on under the hood but there are huge penalty's with non-class based patterns that allow for getter/setter overrides. Unless someone can show me a pattern that works (it's a hard topic to find information on actually), then I'm running on the assumption that browsers have optimized classes in some special way when it comes to my use case.
I'm guessing the browser translates class code to objects/prototypes for each sub class. Meaning that it builds one generator it re-references for each sub class and thus avoids going through the slow mixin process that assigns/joins objects on every instance in a factory/prototype pattern. (You need mixins to override getters/setters as far as I know, but I'd love some guidance if another way).
Nice rant.
There's nothing wrong with organizing your code in classes. It's actually a very good way when language supports it. I'd say it is the best way generally. Everyone understands classes. If they don't understand classes, are they even really a programmer?
So? There's nothing wrong with having a class even when you create only one instance, same as there is nothing wrong with having a closure when you only have only one instance. I often enough create classes in other languages of which I only have one instance at runtime but it makes it easier to test the module(dependency injection and substitution with mocks or objects with specific state in tests).
Anyway, in your rant about classes vs closures this paragraph of yours doesn't add any value and isn't an argument against classes in the classes vs closures debate. Having one instance at runtime is just fine in either case. Moving on.
Having fields is not an attribute of an object in OOP. And it's easy to demonstrate. Take any interface, then create an object that implements that interface and make that object save its state in the database by calling some static functions in a namespace. Now you have an object that fulfils your requirements, implements an interface, stores its state in a database. You can program against an interface, use that object successfully, yet, that object has no fields.
Wrong again. Not long ago I've created a
IKeyboardGateway
interface with methodGetKeyboardState
and created an implementation of that interface:KeyboardGateway
that returns an array of size 255 with the state for each key of the keyboard. TheKeyboardGateway
object didn't operate on an object's internal state. TheKeyboardGateway
object called system'sGetKeyState
function(Windows in this case for me).Other modules that used
IKeyboardGateway
object didn't know it was implemented using theKeyboardGateway
class. All the other modules knew isIKeyboardGateway
and about one method:GetKeyboardState
. That's where OOP is happening: in programming against interfaces. Not in creating classes with a bunch of fields and methods. You can have solutions with classes all over the place in each and every file, and have nothing other than classes, and do no object-oriented programming: all of your dependencies will be hardcoded to specific implementations.Uncle Bob nicely explains what OOP is and what an object is here:
youtu.be/oar-T2KovwE?t=3246
Yeah, there's nothing wrong with using classes in JS. And yeah you can write very bad code with classes. Same as with closures.
That's too vague and needs some concrete example which would demonstrate that. And I doubt it will happen. If we implement the same thing with closure, it'll be just as hard to understand.
JS modules aren't alternative to classes in JS. Alternative to classes is closures. In your example later you provided alternative solution with closure, not module. Module can export either a class or a closure.
False again. You don't need to declare fields in a class(outside of functions). You can just set them in a constructor, same way you did in a closure. You'll just have to add
this.
.This is additional boilerplate code: you need to return a whole struct with variables in it.
In classes you declare private fields with
this.#myVariableName
and in closures you return the struct with public fields available for use. That's around the same amount of boilerplate code.No. It's not about redundant code. It's about managing the dependencies.