Find me on medium
Working with collections in JavaScript can become an appalling task especially when there is a lot going on in a function block.
Have you ever wondered how some projects in code look much nicer than others? Or when a seemingly difficult project ends up being so small your mind just goes off in a wild ride wondering just how they were able to keep it simple and robust at the same time?
When a project is easy to read while maintaining good performance you can be ensured that there's likely pretty good practices applied to the code.
It can easily become the contrary when code is written like a mess. At this point it's easy to get in a situation where modifying small bits of code ends up causing catastrophic problems to your application--in other words an error thrown that crashes a web page from continuing further. When iterating over collections it can become scary to watch bad code run.
Enforcing better practices is about inhibiting yourself from taking short directions which in turn helps to secure guarantees. This means that it depends on you to make your code as maintainable as possible in the long run.
This article will go over 5 anti-patterns to avoid when working with collections in JavaScript
A lot of the code examples in this article will embody a programming paradigm called functional programming. Functional programming, as Eric Elliot explains it, "is the process of building software by composing pure functions, avoiding shared state, mutable data, and side-effects.". We will often mention side effects and mutation in this post.
Here are ___ Anti-Patterns in JavaScript to Avoid When Working With Collections:
1. Prematurely passing functions as direct arguments
The first anti-pattern that we will be going over is prematurely passing functions as a direct argument to array methods that loop over collections.
Here is a simple example of that:
function add(nums, callback) {
const result = nums[0] + nums[1]
console.log(result)
if (callback) {
callback(result)
}
}
const numbers = [[1, 2], [2, 2], [18, 1], [4, 5], [8, 9], [0, 0]]
numbers.forEach(add)
So why's this an anti-pattern?
Most developers especially those who are more into functional programming may find this to be clean, concise and performant at its best. I mean, just look at it. Instead of having to do this:
const numbers = [[1, 2], [2, 2], [18, 1], [4, 5], [8, 9], [0, 0]]
numbers.forEach(function(nums, callback) {
const result = nums[0] + nums[1]
console.log(result)
if (callback) {
callback(result)
}
})
It's seemingly much nicer to just throw in the name of the function and call it a day:
const numbers = [[1, 2], [2, 2], [18, 1], [4, 5], [8, 9], [0, 0]]
numbers.forEach(add)
In a perfect world, this would be the perfect solution to work with all of our functions in JavaScript without ever having to break a sweat.
But it turns out that prematurely passing your handlers this way can cause unexpected errors. For example, lets go ahead and look back into our previous example:
function add(nums, callback) {
const result = nums[0] + nums[1]
console.log(result)
if (callback) {
callback(result)
}
}
const numbers = [[1, 2], [2, 2], [18, 1], [4, 5], [8, 9], [0, 0]]
numbers.forEach(add)
Our add
function expects an array where the first and second indexes are numbers and adds them and checks if there is a callback, invoking it if it exists. The problem here is that callback
could end up being invoked as a number
and will result in an error:
2. Relying on the ordering of iterator functions like .map
and .filter
JavaScript's basic functions process elements in collections in the order they're currently at in the array. However, your code should not depend on this.
First, the ordering of iteration is never 100% stable in every language nor in every library. It's a good practice to treat every iteratee function as if they are run concurrently in multiple processes.
I've seen code that do something like this:
let count = 0
frogs.forEach((frog) => {
if (count === frogs.length - 1) {
window.alert(
`You have reached the last frog. There a total of ${count} frogs`,
)
}
count++
})
In most situations this is perfectly fine, but if we look closely it's not the safest approach to take as anything in the global scope can update count
. If this happens and count
ends up being decremented accidentally somewhere in the code, then window.alert
will never be able to run!
It can get even worse when working in asynchronous operations:
function someAsyncFunc(timeout) {
return new Promise((resolve) => {
setTimeout(() => {
resolve()
}, timeout)
})
}
const promises = [someAsyncFunc, someAsyncFunc, someAsyncFunc, someAsyncFunc]
let count = 0
promises.forEach((promise) => {
count++
promise(count).then(() => {
console.log(count)
})
})
The result:
Those of you who are more experienced in JavaScript will probably know why we get four number 4
's logged to the console and not 1, 2, 3, 4
. The point is that it's a better to use the second argument (commonly referred to as the current index
) that most functions receive when iterating over collections to avoid concurrency:
promises.forEach((promise, index) => {
promise(index).then(() => {
console.log(index)
})
})
The result:
3. Optimizing Prematurely
When you're looking to optimize what usually comes in between is your decision in choosing whether to prefer readability or speed. Sometimes it can become really tempting to put more attention to optimizing your app's speed instead of improving the readability of your code. After all, it's a widely accepted truth that speed in websites matter. But this is actually a bad practice.
For one, collections in JavaScript are usually smaller than you'd think, and the time it takes it to process every operation is also faster than you'd think as well. A good rule to follow here is that unless you know something is going to be slow, don't try to make it faster. This is called Premature Optimization, or in other words, attempting to optimize code that is possibly already most optimal in speed.
As Donald Knuth puts it, "The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming.".
In a lot of situations it's easier to apply some better speed where the code ends up being a little slower than it is having to stress out maintaining a fast working code in a tangled mess.
I recommend to prefer readability, and then proceeding to measure. If you use a profiler and it reports a bottleneck in your application, optimize that bit only because now you know its actually a slow code, as opposed to attempting to optimize code where you think it could be slow.
4. Relying on state
State is a very important concept in programming because it is a concept that enables us to build robust applications but it can also break our applications if we don't watch ourselves enough.
Here is an example of an anti-pattern when working with state in collections:
let toadsCount = 0
frogs.forEach((frog) => {
if (frog.skin === 'dry') {
toadsCount++
}
})
This is an example of a side effect, something definitely to watch out for as it can cause problems like:
- Producing unexpected side effects (Really dangerous!)
- Increasing memory usage
- Reducing your app's performance
- Making your code harder to read/understand
- Making it harder to test your code
So what's a better way to write this without causing a side effect? Or how can we rewrite this using a better practice?
When working with collections and we need to work with state during the operation, remember that we can utilize certain methods that provide you with a fresh new reference of something (like objects).
An example is using the .reduce
method:
const toadsCount = frogs.reduce((accumulator, frog) => {
if (newFrog.skin === 'dry') {
accumulator++
}
return accumulator
}, 0)
So what's happening here is that we're interacting with some state inside its block but we also utilize the second argument to .reduce
where the value can be newly created upon initialization. This is using a better approach than the previous snippet because we're not mutating anything outside of the scope. This makes our toadsCount
an example of working with immutable collections and avoiding side effects.
5. Mutating Arguments
To mutate something means to change in form or in nature. This is an important concept to pay close attention to in JavaScript especially in the context of functional programming. Something that is mutable can be changed while something that is immutable cannot (or should not) be changed.
Here's an example:
const frogs = [
{ name: 'tony', isToad: false },
{ name: 'bobby', isToad: true },
{ name: 'lisa', isToad: false },
{ name: 'sally', isToad: true },
]
const toToads = frogs.map((frog) => {
if (!frog.isToad) {
frog.isToad = true
}
return frog
})
We're expecting the value of toToads
to return a new array of frogs
that were all converted to toads by flipping their isToad
property to true
.
But this is where it becomes a little chilling: When we mutated some of the frog
objects by doing this: frog.isToad = true
, we also unintentionally mutated them inside the frogs
array!
We can see that frogs
are now all toads because it was mutated:
This happens because objects in JavaScript are all passed by references! What if we assigned the same object around in 10 different places in code?
If we for example were assigning this reference to 10 different variables throughout our code, then mutated variable 7 at some point later in the code, all of the other variables that hold a reference to this same pointer in memory will also be mutated:
const bobby = {
name: 'bobby',
age: 15,
gender: 'male',
}
function stepOneYearIntoFuture(person) {
person.age++
return person
}
const doppleGanger = bobby
const doppleGanger2 = bobby
const doppleGanger3 = bobby
const doppleGanger4 = bobby
const doppleGanger5 = bobby
const doppleGanger6 = bobby
const doppleGanger7 = bobby
const doppleGanger8 = bobby
const doppleGanger9 = bobby
const doppleGanger10 = bobby
stepOneYearIntoFuture(doppleGanger7)
console.log(doppleGanger)
console.log(doppleGanger2)
console.log(doppleGanger4)
console.log(doppleGanger7)
console.log(doppleGanger10)
doppleGanger5.age = 3
console.log(doppleGanger)
console.log(doppleGanger2)
console.log(doppleGanger4)
console.log(doppleGanger7)
console.log(doppleGanger10)
Result:
What we can do instead is to create new references each time we want to mutate them:
const doppleGanger = { ...bobby }
const doppleGanger2 = { ...bobby }
const doppleGanger3 = { ...bobby }
const doppleGanger4 = { ...bobby }
const doppleGanger5 = { ...bobby }
const doppleGanger6 = { ...bobby }
const doppleGanger7 = { ...bobby }
const doppleGanger8 = { ...bobby }
const doppleGanger9 = { ...bobby }
const doppleGanger10 = { ...bobby }
Result:
Conclusion
And that concludes the end of this post! I found you found this to be valuable and look out for more in the future!
Find me on medium
Top comments (6)
In section 1 you say:
I'm curious to know how many advocates of FP would write an
add
function in that way; where you use a callback to act on the result. You'd normally just return the result directly and avoid things that might cause unexpected side-effects (such as callback functions).Also the uncondensed version is subject to the same bug:
numbers.forEach(function(nums, callback) {
The problem you demonstrate is that you need to be familiar with the argument signature passed from Array iteration functions. It's true that passing a function name means you may be less likely to spot the issue; but in my experience it's also rare to not write the function inline anyway.
This is a misguided article. I like the idea but the actual examples are poor. It may lead beginners astray, which is more dangerous.
Thanks for this nice article!
What could be the possible solution for the problem no. 1, Prematurely passing functions as direct arguments ?
I could n't perceive. Sorry.
Perhaps an inline function there would be less confusing if it's a one-time, otherwise having a function taking the array as a parameter and output an array as sums, and having a clear name what it does.
If an input is a heavy huge array, using simple for-loop or while-loop for me is the best choice for optimized performance.
I see that one as lack of information. It doesn't matter if you pass the reference to the function or create it inline, the function signature is what is causing the problem. The solution is to search documentation about both function and make sure they are compatible.