Photo by Nathan Dumlao on Unsplash
When interviewing software engineering candidates, one small but important detail I look for is whether or not they are programming idiomatically. One way a programmer can write idiomatic code is by using the tools and conventions built into the language rather than explicitly writing them from scratch. Another way to explain this is by adhering to the phrase: “Don’t reinvent the wheel.” Again, it’s a small detail, but it shows experience and command of the language at hand.
JavaScript has a unique, rambling history that is still being written. To understand how the reduce method works, one must first understand how prototypal inheritance works in JavaScript.
Prototype Methods
Practically everything in JavaScript is an object. Even primitives instantiated literally (i.e. not constructed from a class
), like string literals and number literals, get some object-like behaviors. These behaviors are inherited from the instance’s prototype
. This inheritance can be chained as well. For instance, the array prototype inherits from the object prototype.
The Array.prototype
has several methods that make working with arrays very convenient. The contents of arrays are ordered, so naturally, when working with arrays, you’ll often need to iterate through them. Array.prototype.reduce
is one of these methods and it allows us to declaratively iterate through the array while also leaving the logic to be passed in as a parameter.
The Reduce Method Explained
When I was first learning JavaScript, I tended to avoid using Array.prototype.reduce
partly because I wasn’t confident in my understanding of it. I should have dedicated the time to construct a solid mental model instead of relying on less idiomatic ways to solve problems that the reduce method is ideal for.
Under the Hood
One of the best ways to understand what’s going on is to build a flowchart. Once the method is broken down into its atomic components, it appears much less intimidating.
A flowchart illustrating the logic of Array.prototype.reduce.
Another really good way to understand the reduce method is to reinforce your mental model by rewriting the method’s logic from scratch. When I attended Hack Reactor, this was one of the many introductory tasks that we were assigned. Let’s take a look at some code.
A custom implementation of Array.prototype.reduce.
In the code above, I’ve reassigned Array.prototype.reduce
to my new function so that I can consume it as I normally would consume the reduce method. That means we’ll invoke this method using property access on an array, and therefore, our this
keyword will be assigned to the array we’ve invoked our method on. Throughout this code, you can expect this
to be the array you’re trying to reduce. Let’s walk through the code.
I declare an accumulator
variable and initialize it to either the initial value passed in or the first item in the array. Similarly, I initialize an index
variable to be 1
if we aren’t given an initial value, or 0
otherwise. From there I simply iterate through the array and on each iteration I reassign the accumulator
to be equal to the returned value from the reducer
function.
Gotchas
The best way to remember a programming concept is to spend hours debugging an issue caused by that concept. Over the years, I’ve had a few of these types of experiences myself.
Remember to Return the Accumulator
Early on when I was interviewing at companies to land my first software engineering job, I found myself in a final-stage, onsite interview where I decided to use Array.prototype.reduce
in my solution to a coding problem. In-person coding problems can be stressful and can cause you to make mistakes that you normally wouldn’t. In this case, I forgot to return a value from my reducer function. It was in the early days of ES6 and I was using an arrow function. However, by using a code block (i.e. curly braces) to define my function body, I forgot that I would no longer benefit from the implicit return built into arrow functions that don’t use code blocks. Like this:
An example of not returning a value from the reducer function.
When you do not return a value from your reducer function, the accumulator is reassigned to be undefined
. This occurs because when you don’t explicitly return a value from a function, undefined
is implicitly returned. So the accumulator is just repeatedly reassigned to be undefined
as the array is iterated over.
Instead, I could have used the implicit return from an inline arrow function, or I could have added a return statement.
Examples of properly returning a value from the reducer function.
Remember that the Accumulator Defaults to the First Item
Another common mistake that occurs when using the reduce method is forgetting to pass in the initial value parameter. If you don’t invoke the reduce method with a 2nd parameter, the accumulator is defaulted to the first item in the array and the loop starts iterating at the 2nd item in the array.
In our summing example, this isn’t a problem because we arrive at the same sum.
An example of using the reduce method without a 2nd parameter.
However, when the accumulator has a different structure or is a different JavaScript type altogether, not initializing your accumulator can introduce a bug.
Examples of using the reduce method with and without the 2nd parameter.
In our examples above, we’re trying to sum the deposits. Each deposit is an object that contains an amount
property. Because the accumulator is intended to be a number while the array items are objects, when we don’t initialize our accumulator, we introduce a bug.
When executing the first part of our code, the JavaScript interpreter is forced to try and add an object with a number. The result is a string because the interpreter will coerce both the object and the number into strings and concatenate them together. When we initialize our accumulator to 0
, we get the desired result.
When to Use Reduce
In order to write code idiomatically, you’ll need to have a strong understanding of what the language at hand is capable of. With JavaScript arrays, there are several prototype methods that need can be used in various situations. For reduce, there are a few key scenarios I like to use it with.
Converting an Array to Another Data Structure
Often when working with arrays, you’ll need to convert that list of items into a single value. Whether it’s working with an array of numbers that you need to sum, or sifting through a list of portfolios to count the number of them using a particular strategy, using the reduce method may be the most idiomatic option.
Other times you’ll need to convert an array to an object or a map. Property access on objects and maps is constant time complexity (i.e. O(1)
), so sometimes it is optimal to iterate through the array and map those items in order to reference that mapping later.
When Chaining Multiple Array Methods Together Would be Computationally Expensive
I often have to work with datasets with several hundreds of thousands of rows. Depending on the resources available in the runtime environment, iterating through that many rows can take several seconds or even minutes. That means that an algorithm involving these datasets can’t afford to have nonlinear time complexity, and every optimization counts.
So even if I simply need to chain Array.prototype.filter
and Array.prototype.map
together to filter array items and then map them, iterating through the dataset twice is too much, even though it’s technically linear time complexity (i.e. O(n)
).
So instead of chaining two methods together, we can reduce the array and combine the two pieces of logic together.
A comparison of chaining two array methods versus using reduce.
You’ll notice we arrive at the same answer, but we only need to iterate through the array once by using reduce. Again, this optimization is most beneficial when we have hardware resource constraints.
Conclusion
Write code idiomatically. In order to do that, you’ll need to know and use prototype methods at the right times. The reduce method can be very useful when used correctly, and sometimes it can be very helpful to build your mental model by using flowcharts and code.