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 ofcallbackFn
. For the last invocation, the return value becomes the return value ofreduce()
accumulator: The value resulting from the previous call to
callbackFn
. On first call,initialValue
if specified, otherwise the value ofarray[0]
.currentValue: The value of the current element. On first call, the value of
array[0]
if aninitialValue
was specified, otherwise the value ofarray[1]
currentIndex: The index position of
currentValue
in the array. On first call,0
ifinitialValue
was specified, otherwise1.
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:
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:
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
.
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.
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.
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:
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