Understanding Basics of Reduce in JavaScript

Understanding Basics of Reduce in JavaScript

What we will cover:

Note: This article is updated with corrections about truthy and falsy values

In the previous article, we read about map, filter and find functions, in this article, we will first see how reduce works with a simple example and then try to recreate output with the help of for loop and the concept of callback functions.

Reduce

Let's try to read what MDN says about reduce:

The reduce() method executes a user-supplied "reducer" callback function on each element of the array, in order, passing in the return value from the calculation on the preceding element. The final result of running the reducer across all elements of the array is a single value.

The first time that the callback is run there is no "return value of the previous calculation". If supplied, an initial value may be used in its place. Otherwise, the array element at index 0 is used as the initial value and iteration starts from the next element (index 1 instead of index 0).

Now, this appears a little vague, isn't it?

Let's see one of the syntaxes which will be used in our example:

Note: For ease of understanding we will be just considering the below syntax. You can read more in detail about the complete syntax here which also describes an additional parameter.

// arrow function 
reduce((accumulator, currentValue, currentIndex) => { /* … */ }, initialValue)

// callback function
reduce(callbackFn, initialValue)

// inline callback function
reduce(function (accumulator, currentValue, currentIndex) { /* … */ })

Now hold on, there are multiple names involved here and so let's read some of them and look at what they do:

callbackFn: A function to execute for each element in the array. Its return value becomes the value of the accumulator parameter on the next invocation of callbackFn. For the last invocation, the return value becomes the return value of reduce()

  • accumulator: The value resulting from the previous call to callbackFn. On first call, initialValue if specified, otherwise the value of array[0].

  • currentValue: The value of the current element. On first call, the value of array[0] if an initialValue was specified, otherwise the value of array[1]

  • currentIndex: The index position of currentValue in the array. On first call, 0 if initialValue was specified, otherwise 1.

When we read the above, remember that if there are too many concepts seen, try to break it down into smaller parts and try it with a simpler example, such as below where we are trying to reduce the array to a single value that holds the sum of all the values in the array.

const array1 = [1, 2, 3, 4];

// 0 + 1 + 2 + 3 + 4
const initialValue = 0;
const sumWithInitial = array1.reduce(
  (accumulator, currentValue) => accumulator + currentValue,
  initialValue
);

console.log(sumWithInitial);
// Expected output: 10

Here, as we can see, we are using syntax arrayname.reduce followed by further syntax.

array1.reduce(
  (accumulator, currentValue) => accumulator + currentValue,
  initialValue
);

Here's a breakdown of the syntax and see how it matches the definition:

diagram explaining reduce syntax

For understanding, let's consider acc as the accumulator and curr as the current value.

array1.reduce((acc, curr) => acc + curr,initialValue);

According to the definition then:

for every element, the reducer callback function is called and what does it do?

It takes in two arguments, acc and curr. Then it adds the value at curr element to the acc value and returns it.

Let's visualize this and see what it is trying to do here:

diagram explaining reduce operation when initial value is provided

Here acc+curr is the logic that is used to generate the accumulator value for the next iteration. We can use the accumulator and current values in the present iteration, and after utilizing them both and computing a result, we can then pass the return value of this result to the next iteration.

The next iteration will use the returned value from the previous iteration and pass it to the accumulator argument and then the callback function will execute according to the arguments it receives on each iteration. This will continue till the last element in the array after which, it will send the final value of the accumulator as result.

NOTE: If we do not pass an initial value, there is a slight change in the first iteration on values assigned to accumulator and current value.

A diagram that describes how reduce operation works with an array of numbers when no initial value is provided


Recreating reduce like output

So let's try to recreate this output for this given array with the help of a simple for loop and also try to use the concept of callbacks.

// reduce syntax as seen for above example
array1.reduce(
  (accumulator, currentValue) => accumulator + currentValue,
  initialValue
);

// Here callback function which iterates through elements of array in order can be seen as: 
(accumulator, currentValue) => accumulator + currentValue
// callbackFn takes in two values and returns one value.

Step 1: Defining function and parameters

So first understand here that, the above reduce function works with three values: given array, accumulator and then currentValue . But the accumulator value is created inside the reduce function and is not taken externally. So for our reduce-like function, we will just create a function that takes two parameters, the first parameter will be array on which reduce-like functionality is to be done and the second parameter will take in an initialValue. The reduce-like function should also have a user-defined callback function inside which should work on the array elements. We will define the callback function later.

For now, this is how the reduce-like function will look like which takes two parameters.

function myReduce(numbers,initialValue)

Step 2: Checking initial value present or not

Now we know that the initialValue can be provided or not provided by us. If it is provided, we can use it as it is, if it is not provided we need to do a few changes in how the accumulator is initialized for the first callback. So let's use nullish coalescing operator to see if the value provided is present or not.

function myReduce(numbers,initialValue)  {
const initialValuePresent = initialValue ?? 0; 
// Since it is optional to provide an initial value, let's check if initialValue exists or not using nullish coalescing operator and store the value in a variable. 
}

If the initial value is present, we should proceed with the logic that the initial value exists or otherwise.

function myReduce(numbers,initialValue)  {
const initialValuePresent = initialValue ?? 0; 

if(initialValuePresent)
// proceed with logic that initial value exists 
else 
// proceed with logic that initial value does not exists. 
}

Step 3: Taking care of some initial values

[THIS SECTION IS CORRECTED] Please read the correction note at the end of this blog post.

Note here, what if the initial value is 0? In that case, the above condition will not proceed! Another thing to take care of is that, empty string is falsy value in boolean context so the if condition will not run and it will proceed to the else part that is no initialValuePresent block statement which is not how we want it to be. So to take care of it, we add another condition. Here we check if the initial value is 0 or "" and assign the initialValuePresent variable as 1 to proceed with the logic that the initial value exists. For empty arrays and objects this additional if condition defined for 0 and "" is not needed. (Please see corrections part for better understanding)

const numbers = [1, 2, 3, 4];

function myReduce(numbers, initialValue) {

// checking if initial value is provided or not.   
let initialValuePresent = initialValue ?? 0;

// checking if initial value is 0 or empty string
  if (initialValue === 0 || initialValue === "") initialValuePresent = 1;

if(initialValuePresent)
// proceed with logic that initial value exists 
else 
// proceed with logic that initial value does not exists. 
}

Step 4: Accumulator

Now let's define the third value that the function will use - accumulator. Note here that since the accumulator has to be re-assigned values during callbacks we will declare it with the let keyword. Also depending on the initialValue provided, we will assign the accumulator the desired value later.

const numbers = [1, 2, 3, 4];

function myReduce(numbers, initialValue) {

// checking if initial value is provided or not. 
  let initialValuePresent = initialValue ?? 0;

// checking if initial value is 0 or empty string
  if (initialValue === 0 || initialValue === "") initialValuePresent = 1;

//defining accumulator
  let accumulator;

Step 5: Defining the reducer callback function

Now let's define the reducer callback function that will iterate over the array elements. The function should take in two parameters accumulator value and the currentValue passed to it and then return the sum of it as per the example we have taken.

const numbers = [1, 2, 3, 4];

function myReduce(numbers, initialValue) {

// checking if initial value is provided or not. 
  let initialValuePresent = initialValue ?? 0;

// checking if initial value is 0 or empty string
  if (initialValue === 0 || initialValue === "") initialValuePresent = 1;

//defining accumulator
  let accumulator;

// defining our own call back function: callbackFn 
  const callbackFn = (accumulator, currentValue) => accumulator + currentValue;

Step 6: Understanding accumulator values

Now, that we have assigned the initial value, we need to assign the accumulator value for the iterations to begin.

Here, we have to take care of the fact that the accumulator can either hold the initial value supplied or the value held by the element at index 0 of the array - if no initial value is provided. And so here's a problem after that - once we assign the accumulator - value for the first callback as that of the element at index 0 of the array, we also need to assign the currentValue in that case, the value as present at index 1 in the given array for the first iteration.

So for the first iteration, if no initial value is provided that is initialValue = null then:

accumulator = numbers[0] and currentValue = numbers[1] for first iteration.

And if the initial value is provided, then:

accumulator = initialValue and currentValue = numbers[0] for first iteration.

So we need to consider how we write the above code for assigning value to the accumulator.

const numbers = [1, 2, 3, 4];

function myReduce(numbers, initialValue) {

// checking if initial value is provided or not. 
  let initialValuePresent = initialValue ?? 0;

// checking if initial value is 0 or empty string
  if (initialValue === 0 || initialValue === "") initialValuePresent = 1;

//defining accumulator
  let accumulator;

// defining callbackFn 
  const callbackFn = (accumulator, currentValue) => accumulator + currentValue;

// logic for initial value present
  if (initialValuePresent) {
    accumulator = initialValue;
   currentValue = number[0];

  } else { // logic if initial value not present
    accumulator = numbers[0];
    currentValue = number[1]; 
  }

Step 7: Running iterations over array elements with callback functions

Now that we have all the values, let's understand the iterations for the elements of the array.

// logic for iteration 
for ( let i=0; i<numbers.length; i++)
{ 
 accumulator = callbackFn(accumulator,currentValue);
}

Here, for each iteration, the callback function returns a value that is again stored in accumulator. Combining our understanding from Steps 6 and 7, let's write the iterations for our example.

Step 8: Defining iterations

//defining accumulator
  let accumulator;

// defining callbackFn 
  const callbackFn = (accumulator, currentValue) => accumulator + currentValue;

  if (initialValuePresent) { // logic for initial value present
    accumulator = initialValue;
    for (let i = 0; i < numbers.length; i++) 
    {
      accumulator = callbackFn(accumulator, numbers[i]);
    }

  } else { // logic if initial value not present
    accumulator = numbers[0];
    for (let i = 1; i < numbers.length; i++) 
    {
      accumulator = callbackFn(accumulator, numbers[i]);
    }
  }

As per Step 6 above,

If the initialValue is present: we assigned the accumulator -> the initialValue. And then the callback function will take for the first iteration, the currentValue as the value from the element at the index 0 of the array.

So, since the iterations begin from index 0, we have defined for loop from 0 till the length of the array. Here is how it will work.

diagram explaining how current value and accumulator changes during each iteration when initial value is present.

If the initialValue is present our for loop is like:

for (let i = 0; i < numbers.length; i++)

and also our current value is numbers[i] as seen from the table above. The iterations go from 0 to 3.

accumulator = callbackFn(accumulator, numbers[i]);

Whereas, If the initialValue is not present: then, we assign the accumulator -> the numbers[0]. And then the callback function will take for the first iteration, the currentValue as the value from the element at the index 1 of the array -> numbers[1].

So, since the iterations begin from index 1, we have defined for loop from index 1 till the length of the array. Here is how it will work.

diagram explaining how current value and accumulator changes during each iteration when initial value is not present

If the initialValue is present our for loop is like:

for (let i = 1; i < numbers.length; i++)

and also for our current value, the iterations go from 1 to 3.

accumulator = callbackFn(accumulator, numbers[i]);

Final Step: Return the accumulator

After we have iterated through the elements, return the accumulator value.

// after all iterations return the accumulated value
  return accumulator;

Final recreation of output like reduce function.

So combining all the code we get this function. Let's try it if it works now!

// TEST
// FINAL CODE: Write Javascript code!
const numbers = [1, 2, 3, 4];

function myReduce(numbers, initialValue) {

// checking if initial value is provided or not. 
  let initialValuePresent = initialValue ?? 0;

// checking if initial value is 0 or empty string
  if (initialValue === 0 || initialValue === "") initialValuePresent = 1;

//defining accumulator
  let accumulator;

// defining callbackFn 
  const callbackFn = (accumulator, currentValue) => accumulator + currentValue;

// logic for initial value present
  if (initialValuePresent) {
    accumulator = initialValue;
    for (let i = 0; i < numbers.length; i++) 
    {
      accumulator = callbackFn(accumulator, numbers[i]);
    }

  } else { // logic if initial value not present
    accumulator = numbers[0];
    for (let i = 1; i < numbers.length; i++) 
    {
      accumulator = callbackFn(accumulator, numbers[i]);
    }
  }

// after all iterations return the accumulated value
  return accumulator;
}

console.log(myReduce(numbers));
//  output
10

Conclusion:

So we thus made our own reduce function and understood how reduce works with the help of an example. However, using the inbuilt reduce function is very much preferred since it is capable of handling a variety of cases which otherwise we would need to define every time. To understand the other use cases or read about the implementation of reduce. You can read more here.


Note: For any feedback or correction, you can DM me on Twitter at twitter.com/swapnildecodes

Resources:

Reduce | MDN

Truthy

Falsy

Credits: All diagrams were made with Excalidraw.


Clarification: For ease of understanding and for explaining the example, I have mentioned only 3 parameters in the syntax. You can read more about the complete syntax here. Thanks to Aman Harsh and Gautam Balamurali for pointing it out.


Correction: Please note the erroneous logic published before in Step 3.

An if condition was mentioned previously which compared empty object and array using === operator. This code comparison does not work as intended logically because that's how === operator have been defined to work. This comparison is not needed here. The logic assumed was empty object or array will return falsy in boolean context.

// You can test the output for each. 
let initialValue = {};
let initialValuePresent = initialValue ?? 0;
//  let initialValue = [];
//  let initialValue = 0;
//  let initialValue = "";

if (initialValuePresent  === 0 ||
    initialValuePresent  === '' ||
    initialValuePresent  === [] ||
    initialValuePresent  === {} ) 
    console.log("If comparison is true")
else console.log("If comparison is false")

// It returns "If comparison is false" for empty object and array.

The correction:

Here it is important to note that, nullish coalescing operator assigns the empty object or array or string or 0 to the initialValuePresent variable and does not evaluate it to null or undefined. Further after checking the empty array or object with the if condition, it confirms that some initial value is passed as it evaluates to truthy. So we can conclude that we need not check additionally for empty array or object that are passed as initial value in the context of this blog post.

let initialValue0 = {};
const initialValuePresent0 = initialValue0 ?? 0;
if(initialValuePresent0) console.log("empty object true")
else console.log("empty object false")
console.log(Object.keys(initialValuePresent0).length)
// true

let initialValue1 = [];
const initialValuePresent1 = initialValue1 ?? 0;
if(initialValuePresent1) console.log("empty array true")
else console.log("empty array false")
console.log(initialValuePresent1.length)
//true

let initialValue2 = "";
const initialValuePresent2 = initialValue2 ?? 0;
if(initialValuePresent2) console.log("empty string true")
else console.log("empty string false")
console.log(initialValuePresent2.length)
// false because empty string is checked in boolean context and is falsy 

let initialValueZero = 0;
const initialValuePresentZero = initialValueZero ?? 0;
if(initialValuePresentZero) console.log("zero is true")
else console.log("zero is false")
console.log(initialValuePresentZero.length)
// false because 0 is checked in boolean context and is falsy