How to rewrite classes using closures in JavaScript
Update
I’m very humbled by the insightful discussion on HackerNews. Really, thank you! I have now updated the blog post to address the comments. You can always view the diff on GitHub.
Introduction
As much as I dislike the class
syntax in JavaScript, it used to be my default choice when I needed an object factory, with support for static and instance level properties.
I recently found a clean way to eliminate the class
syntax while still maintaining these properties, and in this post, I’ll show you how.
Classy issues
Classes are plagued with issues, such as:
this
is awkward
Having to write:
this.progressBar.addEventListener(this.handler.bind(this));
is much worse compared to:
progressBar.addEventListener(handler);
The this
prefix is mandatory for every instance property, which increases code bloat. When passing methods around, you have to carefully rebind them to the correct object.
Regarding private properties
I initially wrote that classes don’t support private properties. That was completely wrong, as this MDN article mentions. Using private properties helps better encapsulate internal logic, that we don’t want external code to depend on. I’m glad JS classes support that.
Regarding readonly properties
I initially wrote that we cannot have instance properties that are public but readonly. For example, this is valid JavaScript:
class MyClass {
static prop = 123;
}
MyClass.prop = 456;
As comments pointed out, you can define a get
ter instead:
class MyClass {
static get prop() {
return 123;
}
}
MyClass.prop = 456; // doesn't change the value
Poor bundler optimization
If you are building a large class encapsulating some complex logic, you likely have dozens of private methods as “helper” methods, but only a few externally exposed methods. Unfortunately, build tools like terser
or webpack
cannot optimize them well. For example:
class MyClass {
constructor() {
this.prop = 5;
}
publicMethod() {
this.property = 6;
}
}
const unused = 5;
const instance = new MyClass();
console.log(instance.prop);
When optimized with npx webpack --mode production
, this results in:
(()=>{const o=new class{constructor(){this.prop=5}publicMethod(){this.property=6}};console.log(o.prop)})();
Notice how: 1. the unused instance method is not removed, and 2. the name of this instance method is not simplified.
Poor linting experience
Unused methods and properties are never flagged by eslint
(here’s an example class). This makes it difficult to refactor existing code.
// flagged
const unusedVar = 123;
class MyClass {
// not flagged
static unusedStaticProperty = 6;
constructor() {
// not flagged
this.unusedProperty = 5;
}
// not flagged
unusedMethod() {
}
}
new MyClass();
Not hoisted up
As JavaScript developers, we expect functions (that are blocks of code) to be hoisted up in their own lexical scope. Sadly, classes do not share this same property, which leads to workarounds like these:
var MyClass = class MyClassInternal {
// ...
};
This is not an issue when using module-oriented development. But if you are not using modules, then this becomes a hassle.
Example
Here is an example class that we can rewrite:
class Dog {
static AVERAGE_HEIGHT_FT = 4;
static AVERAGE_WEIGHT_KG = 100;
static _PRIVATE_MAGIC_HEIGHT = 3.14;
constructor(height, weight) {
this.height = height;
this.weight = weight;
if (this.height === this._PRIVATE_MAGIC_HEIGHT) {
this._privateMakeTaller();
}
}
_privateMakeTaller() {
this.height = Dog.AVERAGE_HEIGHT_FT + 1;
}
printHeight() {
const status = this.height > Dog.AVERAGE_HEIGHT_FT ? 'taller' : 'not taller';
console.log(`Your dog is ${this.height} ft tall. The dog is ${status} than the average height`);
}
static getAverageHeight() {
console.log(`Average height for dogs is ${Dog.AVERAGE_HEIGHT_FT} ft`);
}
}
This code has a mix of static and instance-level methods and properties. Let us assume we want to expose only three methods: constructor
, printHeight
and getAverageHeight
. Notice the weight properties are unused, which we might automatically detect in our re-written version. We also have a secret method and a secret property, that we want to encapsulate well.
Closures to the rescue!
JavaScript closures are amazing. We can use them to emulate static properties, instance properties, private properties, as well as readonly properties. Here’s the rewritten version of the above class:
const Dog = (function createDogClass() {
const AVERAGE_HEIGHT_FT = 4;
const AVERAGE_WEIGHT_KG = 100;
const _PRIVATE_MAGIC_HEIGHT = 3.14;
function init(heightInput, weightInput) {
let height = heightInput;
const weight = weightInput;
if (height === _PRIVATE_MAGIC_HEIGHT) {
_privateMakeTaller();
}
function _privateMakeTaller() {
height = AVERAGE_HEIGHT_FT + 1;
}
function printHeight() {
const status = height > AVERAGE_HEIGHT_FT ? 'taller' : 'not taller';
console.log(`Your dog is ${height} ft tall. The dog is ${status} than the average height`);
}
return {
printHeight,
};
}
function getAverageHeight() {
console.log(`Average height for dogs is ${AVERAGE_HEIGHT_FT} kg`);
}
return {
init,
getAverageHeight,
};
})();
Immediately, we see several advantages:
- Unused properties are flagged by eslint (see example)
- Webpack optimizes our code with variable renaming and dead code removal.
- We have lesser code bloat thanks to removing
this.
andDog.
prefixes. -
Our private and readonly properties are truly private and readonly now.As we discussed earlier, regular classes also support this.
How does this work?
Static scope
The scope inside createDogClass
is the static
scope. Variables and functions declared in this scope are shared by all instances. From this scope, we return an object of properties that are exposed externally. In this case, we return:
init
which acts as our constructor, andgetAverageHeight
which lets the users know the average height of dogs.
Notice how:
- the magic height property is private, and
- the average height property is public (exposed via
getAverageHeight
) but readonly
Instance-level scope
We replaced the constructor
with an init
function that does the same job. Now, to create an instance, we call Dog.init(...)
(instead of new Dog(...)
). Note that each invocation of init
returns a new object instance, which also comes up with a separate lexical scope (very handy for us!)
From inside init
, we return all publicly exposed properties. In this case, we only expose printHeight
. Notice how:
_privateMakeTaller
is a private method, and unique for every instance ofDog
- the static properties from
Dog
scope are easily accessible. height
andweight
values are not shared across instances (unique per instance)
Conclusion
I hope this post helped you understand how to rewrite JavaScript class
syntax into closures.
Post conclusion
Motivated by the HN comments, here’s some additional considerations when using closure syntax to emulate classes:
- Developer experience takes a hit because now the
instanceof
check no longer works. Further, the syntaxDog.init
might be strange to use instead of the familiarnew Dog
- Every new object ships with a copy of all the methods, which is bad for memory optimization of the program.
Also, I don’t recommend that we start switching all classes to closures straightaway. With this blog post, I want to highlight the difference between the class pattern vs the closure pattern, and how each of them have their differences and advantages. The choice of which one to use is ultimately yours to make.
Further discussion
Mika Genic reached out to me via e-mail to provide a clever design that clearly defines a public interface while also avoiding an IIFE. Here’s an example:
function Dog(name) {
// define public interface
const self = Object.assign(this, {
publicFn1,
})
// init private state
let created = new Date()
// call constructor
constructor()
return self
// logic
function constructor() { console.log(`dog created`) }
function publicFn1() { console.log(`${privateFn1()}`) }
function privateFn1() { return `${name} ${created}` }
}
let dog = new Dog(`Billy`)
dog.publicFn1()
Note that:
- there is no IIFE, which makes understanding the code far simpler.
- the public interface is clearly separated from the rest of the logic
- You can use the traditional
new
keyword to construct objects from this class. - You can avoid
this
entirely.
Some readers will notice that this is very similar to the pre-ES6 way of declaring classes. Here’s an example post on StackOverflow.