The single biggest obstacle of understanding a piece of code is a lot of variables. Variables introduce state, which in turn increase complexity exponentially. Every single bit of variable information makes reasoning and understanding the code harder.
A single boolean can have two states. Two booleans have four. If you have ten inside your function, they can have 1024 different states. This is far beyond what people can understand. Properly scoping your code and using constants instead of vars greatly increase the readability. And that’s what matters in the long run in almost all situations.
Imperative programming is still the mainstream way of coding today. It is how computers work naturally - they execute operations. Imperative code is optimized for the computers, and not for the people.
Coding in an imperative way is also quite easy in most cases. There are only a few constraints, and the architecture mostly dictates how you structure the code. It is easier to write than to read. But taking the full lifecycle into account, code is like a book - write once, read many. Surprisingly, lines of code does not matter much; as long as the code is readable and easily modifiable, it’s fine if it’s longer than necessary.
Javascipt is inherently an imperative language. It also has a dynamic typing system. In statically typed languages, the type system offers some guidance about the variables. But we have a lot less safety here. This is why people are finding ways to remedy this. First, there was JSDoc annotations, then Flow, and now TypeScript. Their purpose is to give developers the comfort they got used to in other languages. Linting also helps to some extent, as it can spot some well-known bogus structures.
JS Tips newsletter
Subscribe now and get it
Leaving variables behind
Functional languages does not have the notion of variables. All they have are values, which are effectively constants. All collections are also immutable. This sounds counterintuitive, but it allows structure reuse, making operations more effective without sacrificing the nice properties of immutability.
The common counter argument from people coming from imperative languages is that functional-style coding is less effective and wastes a lot of computer resources. If a collection can be modified in-place with a simple for loop, copying and recreating the whole structure in every pass increases the running complexity.
But in practice, 99% of the time, you just can’t notice the difference. If you fire up a profiler and watch close enough, you might see a particular part of the code to run 3 ms instead of 1, but you won’t notice the lag after a button click. There are certain situations, like for complex mathematical calculations, where there is a big difference. But keep in mind that immutable structures might come handy. For example if you use the React framework, you can safely ignore a subtree if nothing is changed, and won’t encounter those nasty bugs where things don’t update when they should.
Focus on those parts that you see as slow, and don’t prematurely optimize. Every optimization comes with a cost, as it is basically a shift in readability from people to computers. If your implementation is already fast, then keep the code clean and readable.
How to do it
The first and foremost thing you can do is to use const every place you would use var. With the help of a suitable linter (like ESLint, with the no-const-assign rule), you can spot invalid reassigns in compile time. This makes spotting bugs easier.
Let’s consider the following example. We have a bunch of penguins, and we are interested in the average age of the males. Written in an imperative style, it would look like this:
In contrast, a more functional style solution for the same problem would be something like this:
Apart from being a bit shorter than the first solution, it has a definitive advantage that if you delete any line (except for the returns), the linter would immediately let you know.
A side effect is that the code is easily copy-pasteable. If something is missing, there will be a warning, and you would immediately know.
Also it’s easy to spot what is not needed. If a const is declared but never used, you are safe to delete it. You can use the no-unused-vars rule of ESLint to detect this. In contrast with imperative code, where in most situations it’s quite hard to detect this. Let’s consider the following code, where a variable is read and set, but never actually used:
In this code, the lastDigit variable is set and read, so that the linter won’t detect it as useless. But if you don’t use it ever after, you could safely delete it.
- Move the side effects to an easily identifiable place, for example to the bottom. It would help the reader identify which parts he can safely refactor and which are more dangerous places. Don’t modify anything in a closure, as people would not expect side effects there.
- Only short functions have side effects. If you have a long one, move the computations out into a pure function, and call that first. This makes the harder parts easily understandable.
Using consts promotes proper scoping of your values. Since you can’t reassign a variable, you’ll find yourself using IIFEs (immediately-invoked function expression) a lot.
The most important step is to learn the functional methods for collections. These include filter, map, reduce, some, every, and some others. Their effective use will make your code shorter and easier to understand for people who are using them too.
Using them promotes a programming pattern called collection pipeline. It’s basically a series of operations on a collection and return the result.
As an example, let’s group the male penguins’ name by age:
In contrast, using the collection pipeline pattern, it would be something like this:
Collection pipeline properly scopes each operation. You don’t need to inspect the whole code to see what criteria you are filtering on, or how each groups are formed. They are in their easily-identifiable position.
When you are reducing an array and need to keep multiple values in the memo, you can use the spread operator. For example to make an aggregated shopping list for an array of cars, you can write:
As arrays are inherently mutable, you need to make sure not to mutate them. Assignments are easier to spot, but push/pop are two mutate functions you might use every now and then. For push, you can use the spread operator, like const b = […a, 3];. For pop, there is no alternative, but many libraries provide this function.
To make sure your arrays are immutable, you can use Object.freeze(). Despite the name, it also freezes arrays, and prevents any modifications. But keep in mind that it only throws an exception in strict mode, otherwise it silently ignores the change. But most likely you are already using strict mode, and if not, you should.
Immutable objects are a bit harder topic, but they are very well doable. The first thing is to use the bracket notation, so that you’ll be able to create objects with variable keys:
Then object concatenation can be done with Object.assign. It creates a shallow copy, but it doesn’t matter, as you are only using immutable values.
As an example on how to create dynamic objects, let’s see the different styles on a shopping list for a car.
The same in a more functional style:
Albeit the first solution is shorter, the functional style is properly scoped. With this approach, you always know what part you should look to figure out how the wheels calculations are done. With the imperative approach, you need to look at the whole code to make sure nothing else changes that particular property. For small codebases the difference is negligible, but as the components begin to grow, it really makes a difference.
Using IIFEs are a lot like refactoring to functions. For example, you could write a getNeededWheels() and getNeededTransmissions() function and call them; it’s the same, but if you only use them once it’s better to keep related things close together.
The methods I used in the examples above are using standard ES6 functions. There are many libraries that provide similar (and more) functionality in a more supported way. You should check out Underscore.js, lodash, or immutable-js.
The spread operators and fat arrow functions are also supported in older browsers if you use a suitable compiler, like Babel.
There are many ways to write readable code. My coding style is heavily influenced by functional languages, but I’ve found these principles to be the cornerstones of clean code. Following them is not trivial and certainly feels odd at first, but your codebase will be more readable in the long run.