Washing your code: avoid loops
You’re reading an excerpt of my upcoming book on clean code for frontend developers, “Washing your code.”
Preorder the book now with 20% discount!
Traditional loops, like
while, are too low-level for common tasks. They are verbose and prone to off-by-one errors. We have to manage the index variable ourselves, and I always make typos with
lenght. They don’t have any particular semantic value beyond telling us that some operation is probably repeated.
Replacing loops with array methods
For example, let’s convert an array of strings to
kebab-case with a
And with the
map() method instead of a
We can shorten it even more if our callback function accepts only one parameter: the value. Take kebabCase from Lodash for example:
This wouldn’t work with functions that accept more than one parameter because
map() also passes an array index as the second parameter and a whole array as the third. Using
parseInt(), a function that accepts the radix as its second parameter, would likely lead to unexpected results:
Here in the first example,
parseInt() with an array index as a radix, which gives an incorrect result. In the second example, we’re explicitly passing only the value to the
parseInt(), so it uses the default radix of 10.
But this may be a bit less readable than the expanded version because we don’t see what exactly we’re passing to a function. ECMAScript 6’s arrow functions made callbacks shorter and less cluttered compared to the old anonymous function syntax:
Now, let’s find an element in an array with a
And now with the
In both cases, I much prefer array methods when compared to
for loops. They are shorter, and we’re not bloating the code with iteration mechanics.
Implied semantics of array methods
Array methods aren’t just shorter and more readable; each method has its own clear semantics:
map()says we’re transforming an array into another array with the same number of elements;
find()says we’re finding a single element in an array;
some()says we’re testing that the condition is true for some elements of the array;
every()says we’re testing that the condition is true for every element of the array.
Traditional loops don’t help with understanding what the code is doing until we read the whole thing.
We’re separating the “what” (our data) from the “how” (how to loop over it). More than that, with array methods we only need to worry about our data, which we’re passing in as a callback function.
When all simple cases are covered by array methods, every time we see a traditional loop, we know that something unusual is going on. And that’s good: fewer chances we’ll miss a bug during code review.
Also, don’t use generic array methods like
forEach() when more specialized array methods would work, and don’t use
forEach() instead of
map() to transform an array.
This is a more cryptic and less semantic implementation of
map(), so better use
map() directly as we did above:
This version is much easier to read because we know that the
map() method transforms an array by keeping the same number of items. And, unlike
forEach(), it doesn’t require a custom implementation or mutating an output array. In addition, the callback function is now pure: it merely transforms input parameters to the output value without any side effects.
We run into similar problems when we abuse array method semantics:
map() method is used to reduce an array to a single value by having a side effect instead of returning a new item value from the callback function.
It’s hard to say what this code is doing, and it feels like there’s a bug: it only cares about the last product in a list.
If it’s indeed a bug, and the intention was to check if some of the products have the expected type, then the
some() array method would be the best choice:
If the behavior of the original code was correct, then we actually don’t need to iterate at all. We can check the latest array item directly:
Both refactored versions make the intention of the code clearer and leave fewer doubts that the code is correct. We can probably make the
isExpectedType variable name more explicit, especially in the second refactoring.
Dealing with side effects
Side effects make code harder to understand because we can no longer treat a function as a black box: a function with side effects doesn’t just transform input to output but can affect the environment in unpredictable ways. Functions with side effects are also hard to test because we’ll need to recreate the environment before each test is run and verify it after.
All array methods mentioned in the previous section, except
forEach(), imply that they don’t have side effects and that only the return value is used. Introducing any side effects into these methods would make code easy to misread since readers won’t be expecting side effects.
forEach() doesn’t return any value, and that’s the right choice for handling side effects when we really need them:
for of loop is even better:
- it doesn’t have any of the problems of regular
forloops, mentioned at the beginning of this chapter;
- we can avoid reassignments and mutations since we don’t have a return value;
- it has clear semantics of iteration over all array elements since we can’t manipulate the number of iterations, like in a regular
forloop. (Well, almost, we can abort the loops with
Let’s rewrite our example using a
for of loop:
Sometimes loops aren’t so bad
Array methods aren’t always better than loops. For example, the
reduce() method often makes code less readable than a regular loop.
Let’s look at this code:
My first reaction would be to rewrite it with
reduce() to avoid loops:
But is it really more readable?
After a cup of coffee and a chat with a colleague, I’ve ended up with a much cleaner approach:
If I was to review such code, I would be happy to pass both versions but would prefer the original with double
for loops. (Though
tableData is a really bad variable name.)
Iterating over objects
map() for objects, though Lodash does have three methods for object iteration, so it’s a good option if we’re already using Lodash in our project.
If we don’t need the result as an object, like in the example above,
Object.entries() are also good:
I don’t have a strong preference between them.
Object.entries() has more verbose syntax, but if we use the value (
names in the example above) more than once, the code would be cleaner than
Object.keys(), where we’d have to write
allNames[race] every time or cache this value into a variable at the beginning of the callback function.
If I stopped here, I’d be lying. Most of the articles about iteration over objects have examples with
console.log(), but in reality, we’d often want to convert an object to another data structure, like in the example with
_.mapValues() above. And that’s where things start getting uglier.
Let’s rewrite our example using
And with a loop:
.reduce() is the least readable option.
In later chapters, I’ll urge you to avoid not only loops but also reassigning variables and mutation. Like loops, they often lead to poor code readability, but sometimes they are the best choice.
But aren’t array methods slow?
One may think that using functions is slower than loops, and likely it is. Most of the time, however, it doesn’t matter unless we’re working with millions of items.
It’s not slow anymore, though, and there are other examples where engines optimize for simpler code patterns and make manual optimization unnecessary.
.findIndex() will short circuit, meaning they won’t iterate over more array elements than necessary.
In any case, we should measure performance to know what to optimize and see whether our changes really make code faster in all important browsers and environments.
Start thinking about:
- Replacing loops with array methods, like
- Avoiding side effects in functions.
Read other sample chapters of the book: