Adrian Li

@adrianmcli

Javascript state encapsulation without classes in 2019

A state container in the wild.

I tend to avoid classes whenever possible in Javascript as I usually prefer a functional style over an object oriented one. But one thing that objects do really well is state encapsulation.

In this article, I’m going to argue for using the function constructor pattern without this as the preferred approach for encapsulating state. This was inspired by the widespread criticism of the private fields TC39 proposal on Reddit.

I’ll be talking about how to encapsulate state:

  1. With the ES2015 class keyword,
  2. With simple functions and object literals,
  3. Without using this and having private fields

At the end of the article, I have a bonus waiting for you, demonstrating composition with this pattern. You might like this if you like React Hooks!

Using ES2015 classes

This is probably the most popular option as of 2019, and I think the following should be pretty easy to understand:

class MyObj1 {
constructor(initVal) {
this.myVal = initVal
}
  set(x) {
this.myVal = x
}
}

Once instantiated:

const x = new MyObj1(0)

You can set the value of myVal with either:

x.set(2)

Or:

x.myVal = 2

Similarly, you can access the field directly:

console.log(x.myVal) // output: 2

This all seems great and the syntax looks very inviting to people familiar with other OOP languages, but there are significant flaws with this approach.

No private fields

The next time that your object field is something you didn’t expect, it’s not so easy to figure out where, when, and how it was changed.

It’s relatively straight-forward, but the lack of private fields make this implementation potentially dangerous. The whole point of state encapsulation is that you want to control what can and cannot alter the state (and how they do it).

But in this case, anyone (or any part of the program) can set x.myVal to anything. The next time that myVal is something you didn’t expect, it’s not so easy to figure out where, when, and how it was changed.

If you had private fields, however, you can have control over the behaviour of setting the variable via the set method on your object.

Extra mental overhead and API surface area

Understanding of the ES2015 class syntax is extra mental overhead for developers. There are on-going proposals to augment this with public and private fields (the syntax of which is still contentious and uncertain).

These proposals make it even more difficult for beginners and even seasoned developers (who don’t actively track TC39 proposals) to use classes in practice.

Classical inheritance

On top of that, the nature of the syntax (via the extends keyword) encourages inheritance over composition and incorrectly suggests the traditional classical inheritance pattern.

Some prior art regarding class and classical inheritance issues with Javascript:

Using simple functions and objects

i.e. “class-free” object oriented programming

So if we’re not using the class keyword, what should we be using?

Simple functions and objects! These are the basic building blocks that even the most junior developers are familiar with.

All we’re doing is writing a function that returns an object literal; not exactly rocket science.

This is how we declare our function:

const MyObj2 = initVal => {
return {
myVal: initVal,
set: function(x) {
this.myVal = x
}
}
}

Notice that you didn’t need to learn about class or constructor at all. It’s just a function that returns an object. We create one like this:

const x = MyObj2(0)

We didn’t even need to use the new keyword because we’re not going to be modifying any prototypes (which is a bit of a code smell in 2019, IMHO). It’s simply a function that returns an object.

Similar to the ES2015 class approach, we can set the value with:

x.set(2)

Or:

x.myVal = 2

This approach still has all the issues of not having private fields, but at least we’re no longer in a different paradigm. I argue that this itself is a big improvement over the ES2015 class approach because there really is nothing new to learn here.

It’s just basic Javascript: Functions and Object Literals

Not using the `this` keyword

The this keyword in Javascript confuses both new and old programmers alike. In older code, you might see this being assigned to self or that. And a quick Google search will reveal numerous articles attempting to explain how the this keyword works in Javascript.

I’m not going to discuss why avoiding the this keyword is preferable, that is an exercise left to the reader since much has already been written about it. Instead, allow me to explain the benefits of an alternate approach.

If we simply use closures, we can define an object-creating function like this:

const MyObj3 = initVal => {
let myVal = initVal
return {
get: function() {
return myVal
},
set: function(val) {
myVal = val
}
}
}

Just like the approach above, we can use it to create an object like this:

const x = MyObj3(0)

The myVal variable is essentially private, which means we can no longer access it using x.myVal like in the other implementations above. Instead we have to call the getter function:

x.get()

In some ways, this is a drawback of this approach, but it can also be considered a benefit when you consider the power and explicitness that it gives you.

Closures enable a strong contract

The variable is conceptually “saved” inside the object returned from the function thanks to Javascript closures. This means that we can set the value with the setter method (i.e. x.set(2)), but attempting to directly change the field (i.e. x.myVal = 2) won’t do anything.

The greatest part of having these explicit setters and getters is that the object now has a strong contract/interface with the outside world. The state is fully encapsulated and the only ways in and out of the object is through the setters and getters.

What about prototypes?

Note that I completely skipped over using prototypes and prototypal inheritance in this conversation. This was deliberate, because modifying prototypes as a pattern has become less and less common in recent years.

And even Douglas Crockford (someone who was previously a proponent of prototypal inheritance) is now recommending “class-free” object oriented programming (i.e. the pattern discussed above).

What about performance?

One drawback about using closures is the issue of performance. While there doesn’t seem to be any difference when creating an object, method calls on the object using closures were about 80% slower.

Admittedly, I didn’t look into this too much, but since the different results are in the same order of magnitude, it might be a case of premature optimization to be too concerned about this.

If you are making millions of method calls per second and performance really matters to you, then it’s a judgement call as to whether or not the sacrifice is worth it for your specific use-case.

Simplicity matters

Personally, I think a smaller API surface area is a good thing for the community as it means less confusion for beginners and more standardized patterns for getting things done.

You don’t have to use all features of a language, and you don’t get extra brownie points for doing so. Sometimes, less is more.

Why add classes when simple functions and plain Javascript objects will do? An understanding of closures is already required when you’re working with functions in Javascript. Interestingly, Redux uses this pattern in their createStore function.

For more on this approach, check out Douglas Crockford’s talk here:

See discussion on inheritance and the function pattern at 41:55.

He adds Object.freeze for further safety, which I would recommend as well. The entire talk is worth watching, but pay special attention to the part where he starts talking about classes at 41:55 onwards.

As we go forward in 2019, let’s commit to thinking critically about how we code and the language features we are using. Sometimes it might benefit us to use less rather than more.

For the full code, check out the repo here:

Bonus: A counter example with composition!

Warning!!! This might remind you of React Hooks :)

More by Adrian Li

Topics of interest

More Related Stories