RemixNode's Blog

TwitterGitHub
Build your JavaScript Muscles with map, reduce, filter and other array iterators

Build your JavaScript Muscles with map, reduce, filter and other array iterators

A basic definition of an Array goes like,

An array is a special variable, which can hold more than one value at a time.

Arrays in JavaScript, are single variables used to store different kind of elements. One of the primary needs in dealing with an Array is to iterate through and work with the array elements.

There are several ways to iterate through JavaScript Arrays. Language provided loops like, for, forEach, for-of, for...in(careful here) are to solve the problems. However the elegance and power both comes when we iterate through JavaScript Arrays in use-case driven fashion.

As part of this story, I will be explaining Six array Iteration use-cases and how to solve them easily with fundamental support provided by JavaScript language itself. We will be seeing the usage of filter(), map(), reduce(), find(), some() and every() in action along with some of them in combinations.

How to read this

This is going to be bit lengthy. But I am sure, it will be an easy and fun learning experience for you.

  • If you are aware of these concepts already, it is going to be a good brush-up time for you. Feel free to comment with any additional methods, examples that will be a benefit to others as well.
  • If you are getting started with it or haven't used much before, I would suggest you to do some hands-on along with the use-cases explained below. Feel free to read more on internet about each of the methods and frame your own examples as we go.

All the source code used in this story can be found on my GITHub repo. Please fork, use and comment. All set? Let's start building the strong muscles!

Let's Do it with Examples

We will be taking an example of an array of Customer objects. Let us assume that, we have bunch of customer data returned from the server. Each of the Customer has properties like, id, first name, last name, gender, married, age, expense and, the list of purchases made.

An array of Customer objects may look like this (for simplicity, we just have 5 customer records!):

// Customer object
let customers = [
   {
      'id': 001,
      'f_name': 'Abby',
      'l_name': 'Thomas',
      'gender': 'M',
      'married': true,
      'age': 32,
      'expense': 500,
      'purchased': ['Shampoo', 'Toys', 'Book']
   },
   {
      'id': 002,
      'f_name': 'Jerry',
      'l_name': 'Tom',
      'gender': 'M',
      'married': true,
      'age': 64,
      'expense': 100,
      'purchased': ['Stick', 'Blade']
   },
   {
      'id': 003,
      'f_name': 'Dianna',
      'l_name': 'Cherry',
      'gender': 'F',
      'married': true,
      'age': 22,
      'expense': 1500,
      'purchased': ['Lipstik', 'Nail Polish', 'Bag', 'Book']
   },
   {
      'id': 004,
      'f_name': 'Dev',
      'l_name': 'Currian',
      'gender': 'M',
      'married': true,
      'age': 82,
      'expense': 90,
      'purchased': ['Book']
   },
   {
      'id': 005,
      'f_name': 'Maria',
      'l_name': 'Gomes',
      'gender': 'F',
      'married': false,
      'age': 7,
      'expense': 300,
      'purchased': ['Toys']
   }
];

filter.png

Use-Case 1: Get 'Senior Citizens' by Filtering out other customers

Let us assume, a Customer is qualified as a Senior Citizen if his/her age is equal or more than 60.

Approach - Use Array.prototype.filter() method

The Array filter() method takes a callback function which is also called as, test function. The filter() method creates a new array with all elements that pass the test implemented by the provided callback function.

The callback function takes three arguments, current value, current index and the source array itself. The most used syntax looks like:

const newArray = arr.filter((element, index, array) => {
   // Do Something Here...
});

Filtering out non-Senior Citizens

We need a test condition which will define the callback(or test) function for the filter() method. The test condition is, Customer's age >= 60. This will filter out all the customers from the array that are not satisfying the test condition.

Here is the code look like:

// filter example - Build Customer Data for Senior Citizens
const seniorCustomers = customers.filter((customer) => {
   return (customer.age >= 60)
});
console.log('[filter] Senior Customers = ', seniorCustomers);

Output:

[filter] Senior Customers =  [
  {
    id: 2,
    f_name: 'Jerry',
    l_name: 'Tom',
    gender: 'M',
    married: true,
    age: 64,
    expense: 100,
    purchased: [ 'Stick', 'Blade' ]
  },
  {
    id: 4,
    f_name: 'Dev',
    l_name: 'Currian',
    gender: 'M',
    married: true,
    age: 82,
    expense: 90,
    purchased: [ 'Book' ]
  }
]

map.png

Use-Case 2: Transform the customer array with a new attribute

Have you noticed, the customer details in the array do not have a full name or title? In this use-case, we will be transforming the customer array to a new array which will have a title and full name added to it.

Approach - Use Array.prototype.map() method

The map() method creates a new array with the results of a callback function on every element in the calling array. You are free to write any transformation logic in the callback function to create something that suites your use-case.

The callback function takes three arguments, current value, current index and the source array itself. The most used syntax looks like:

const newArray = arr.map((currentValue, index, array) => {
    // Do Something Here...
});

Transform to add title and full name

Here we will be using the Array's map() method to go over each of the customer value and add a full_name property based on certain conditions. At the end, we will get a new customer array where each of the customer object element has full_name added to it.

// map example - Build Customer Data with title and full name
const customersWithFullName = customers.map((customer) => {
   let title = '';
   if(customer.gender === 'M') {
      title = 'Mr.';
   } else if(customer.gender === 'F' && customer.married) {
      title = 'Mrs.';
   } else {
      title = 'Miss';
   }
   customer['full_name'] = title 
                           + " " 
                           + customer.f_name 
                           + " " 
                           + customer.l_name;
   return customer;
});
console.log('[map] Customers With Full Name = '
               , customersWithFullName);

Output - One of the Customer element inside the mapped array will look like this:

// Notice, full_name property added to it.
 {
    id: 1,
    f_name: 'Abby',
    l_name: 'Thomas',
    gender: 'M',
    married: true,
    age: 32,
    expense: 500,
    purchased: [ 'Shampoo', 'Toys', 'Book' ],
    full_name: 'Mr. Abby Thomas'
  }

reduce.png

Use-Case 3: Get the average age of the Customers who purchased 'Book'

If you look into the Customer array once again, you will notice that, our Customers have purchased various things. Now we would like to perform some analytics so, we would like to know the average age of the Customers who have purchased the Item, 'Book'.

Approach - Use Array.prototype.reduce() method

The reduce() method uses a reducer function on each element of the array, returning a single output value.

arr.reduce(
   reducer(
      accumulator, 
      currentValue, 
      index, 
      array),
   initialValue);

The reduce() method takes two arguments:

  • A reducer function which is also called as callback function to be called on each element of the array.
  • An optional initial value. This is used as the first argument to the first call of the reducer function. If no initialValue is provided, the first element in the array will be used and skipped.

    Note: Calling reduce() method on an empty array without an initialValue will throw a TypeError.

As we know about the reduce() method now, let us go deep into the reducer function. The reducer function takes four arguments:

  • An accumulator: It accumulates the reducer's return values. It is the accumulated value returned from the last invocation of the reducer, or initialValue, if supplied.
  • Current Value: The current element of the array.
  • Current Index: The index of the current element of the array.
  • Source Array: Entire source array.

The image below illustrates the behavior of the reducer function well: reducer.png "Your reducer function's returned value is assigned to the accumulator, whose value is remembered across each iteration throughout the array and ultimately becomes the final, single resulting value." - from MDN

Average age of 'Book' buyers

This is how we can use the array reduce() method to compute the average age of all the customers who have purchased 'Book'.

// reduce example - Get the Average Age of 
// Customers who purchased 'Book'
let count = 0;
const total = customers.reduce(
   (accumulator, customer, currentIndex, array) => {
      if(customer.purchased.includes('Book')) {
         accumulator = accumulator + customer.age;
         count = count + 1;
      }
      return (accumulator);
   }, 
0);
console.log('[reduce] Customer Avg age Purchased Book:'
               , Math.floor(total/count));

Output:

[reduce] Customer Avg age Purchased Book: 45

In above example, the initialValue is passed as, 0. The reducer function(which is an arrow function) does the logic of summing up the age. It also finds the count of the such customers to use it for calculating an average later.

some.png

Use-Case 4: Do we have a Young Customer(age less than 10 years)?

Our Customers are span across different age groups. Do we have any customers below 10 years of age? At this point, we are just interested in a 'Yes'/'No' kind of answer.

Approach - Use Array.prototype.some() method

The some() method checks if a specified condition is satisfied for at least one of the elements in the array. The condition is specified by you using a callback function. If the test condition is passed for at least one element of the array, a Boolean true is returned, false otherwise.

 arr.some((element, index, array) => {
  // Do Something Here...
 });

Note: This method returns false for any condition put on an empty array.

Do we have such customers?

const hasYoungCustomer = customers.some((customer) => {
   return (customer.age < 10);
});
console.log('[some] Has Young Customer(Age < 10):', hasYoungCustomer);

Output:

[some] Has Young Customer(Age < 10): true

find.png

Use-Case 5: Who's the Young Customer (age less than 10 years)?

In previous use-case we have seen that, we have a Customer below 10 years age. Let us find out, who's he/she?

Approach - Use Array.prototype.find() method

The find() method returns the first matching element from the array. The match condition to be provided by you as a test/callback function to the find() method. If there is no match, undefined will be returned.

If there are multiple elements matches the criteria, find() method will always return the first match. There is also a findIndex() method which works in similar fashion but, returns the index of the first matched element.

arr.find((currentElement, currentIndex, array) => {
  // Do Something Here...
});

Find the below 10 years old Customer

The test(or callback) function should have a logic like, customer.age < 10. This is how our find() method should look like:

const foundYoungCustomer = customers.find((customer) => {
   return (customer.age < 10);
});
console.log('[find] Found Young Customer(Age < 10): ', foundYoungCustomer);

Output:

[find] Found Young Customer(Age < 10): {
  id: 5,
  f_name: 'Maria',
  l_name: 'Gomes',
  gender: 'F',
  married: false,
  age: 7,
  expense: 300,
  purchased: [ 'Toys' ],
  full_name: 'Miss Maria Gomes'
}

every.png

Use-Case 6: Do we have a Customer without any purchase?

Among all the customers we have got, how do we check if someone is just for the Window Shopping and haven't really purchased anything!

Approach - Use Array.prototype.every() method

The every() method takes a test(or callback) function as an argument to check on every element to determine if all the elements in the array passed the test. If all passed, returns true, else false.

arr.every((currentElement, currentIndex, array) => {
  // Do Something Here...
});

Note: Unlike some(), this method returns true for any condition put on an empty array.

Is there a Window Shopper?

const isThereWindowShopper = customers.every((customer) => {
   return (customer.purchased.length === 0);
})
console.log('[every] Is there a window shopper?', isThereWindowShopper);

Output:

[every] Is there a window shopper? false

As you would have noticed that, all of our customers have purchased at least one item. Hence the every() method helped us correctly to find that, we do not have a Window Shopper yet!


Let us Chain things together

As it happens with any physical exercises, building muscle for just one part of the body may not be that satisfactory. Similarly, if we try using all the methods discussed above in combination, we will be able to solve complex problems easily. Here is an example use-case of how these methods can be chained together to achieve a result.

Get the total amount spent by Married Customers

We will solve this problem in step-by-step.

  • Filter the married customers using filter() method
    const marriedCustomers = customers.filter((customer) => {
     return (customer.married);
    });
    
  • Use the output of the above(married customers) as input to a map() method to get an array of expenses done by them.
    const expenseMapped = marriedCustomers.map((marriedCustomer) => {
     return marriedCustomer.expense;
    });
    
  • Use the output of the above(expenses of married customers) as input to a reduce() method to get total expense.
    const totalExpenseMarriedCustomer = expenseMapped.reduce(
     (accum, expense) => {
     return accum + expense;
    }, 0);
    console.log('Total Expense of Married Customers in INR: '
    , totalExpenseMarriedCustomer);
    
    Output:
    Total Expense of Married Customers in INR:  2190
    
    As you see, we have chained filter() => map() => reduce() methods in above example. But wait, isn't it too much of code? Yes, but we have a better way. The above chain can be written like this:
    const total = customers
                       .filter(customer => customer.married)
                       .map(married => married.expense)
                       .reduce((accum,expense) => accum + expense);
    console.log('Orchestrated total expense in INR: ', total);
    

Sweet, isn't it?

Conclusions

To conclude this story I would like to suggest to try out these cool methods, if not already. There may be a notion of complexity in understanding some of these methods like reduce() or map(). But when you start embracing them and write example code, you will just start loving it!

Credits and Resources

  • Muscle men in the cover and post image are from here
  • Learn about all about JavaScript Array APIs and Methods from MDN
  • Some cool examples using the Array iterator methods.
  • Read about forEach vs map() here

Please like/share the post if it was useful. I would love to hear from you on your experience in using these methods.