You might not need that Class in your JavaScript
Cory Rylan
- 3 minutes
With ES2015 modules and classes, we have some nice language features in JavaScript. Unfortunately with the introduction of classes in JavaScript, I commonly see them being used when they are unnecessary, and some more flexible options are available. In this post, we will compare a few classes and how with using modules we can make our code more flexible and performant in certain use cases.
When starting out with JavaScript coming from languages like C# and Java a common mistake I see is making everything Classes. For example, let's look at this simple String Utility Class I made below.
export class StringUtil {
static reverse(value) {
return value
.split('')
.reverse()
.join('');
}
static greeting(value) {
return `Hello ${value}!`;
}
static camelToKebab(value) {
return value.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
}
}
This class is pretty straightforward, a collection of some simple string utilities. I make the methods static so I can easily import and call those methods without having to create an instance of this class.
import { StringUtil } from './string-util';
console.log(StringUtil.reverse('Hello'));
// olleH
From many languages, this would be pretty common code to see. With JavaScript we have a couple of alternatives that can make it a little bit easier to maintain and get some better performance in browser environments.
Modules
JavaScript's Module system is quite powerful and flexible. The module system allows us to export pretty much any language symbol we chose, variables, class, functions, etc. Since we can export individual functions from JavaScript modules, we can refactor our String Utility Class to use pure functions.
export function reverse(value) {
return value
.split('')
.reverse()
.join('');
}
export function greeting(value) {
return `Hello ${value}!`;
}
export function camelToKebab(value) {
return value.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
}
Now we can import the functions we want to use into our code.
import { greeting, camelToKebab } from './string-util';
console.log(greeting('Cory Rylan'));
// Hello Cory Rylan!
console.log(camelToKebab('camelToKebab'));
// camel-to-kebab
Now, why would we want to use functions? Isn't this more verbose since I have to import each function instead of just one class? Well yes, it can be a bit more verbose, but there are some key benefits of this pattern. First is the performance. Notice we didn't use the reverse
function? We don't want to send that code to our client's machines if we don't use it.
Many JavaScript bundling tools for Web environments like Webpack and Parcel can take advantage of plain functions. When these tools go to package our code for production if they see a function that is not imported anywhere it can simply exclude it from the bundled production code. This optimization called "Tree Shaking" can reduce the size of the JavaScript sent to the client's browser further improving performance. When logic is attached to a Class, it can be more difficult to determine if its safe to remove the dead code without risking side effects.
Other secondary benefits to using pure functions in certain situations it can make unit testing easier. Unit testing pure functions with no state tend to be significantly easier to test. In some situations it may make sense to use classes for rich models, ex const user = new User('Cory', 'Rylan')
. I find using pure functions works great for utility code and library code. For complex stateful business logic behavior Model like Classes can work better. Both have advantages and disadvantages, be pragmatic about when to use either one. Check out a working demo of the code below!