JavaScript Series: Adventures with Arrays | Filter

In the last note we learned about applying a function to an array of elements. There is much more we can do with collections of elements. Ready to explore more advanced topics?

Before we move to some more complex operations there is one thing more we need to learn about.

When map fails

Last time we applied a function returning full name to a collection of objects containing first and last name. Remember the definition?

  
  /**
   * Sample Usage:
   *
   *   fullName({ firstName: 'Jason', lastName: 'Bourne' });
   *   // => 'Jason Bourne'
   *
   */
  function fullName(person) {
    return person.firstName + ' ' + person.lastName;
  }

So long as we want to keep the same amount of results it’s fine to map items. However, sometimes we need to get rid of some of the results. Or, in other words, we want to keep some of them and forget about the others. Imagine an application that would let us filter people by gender. We have two females and one male in our collection:

  
  var people = [
    { firstName: 'Jason', lastName: 'Bourne' },
    { firstName: 'Nicky', lastName: 'Parsons' },
    { firstName: 'Marie', lastName: 'Kreutz' },
  ];

Okay, right, computer doesn’t know this. We should let it know:


  var people = [
    { firstName: 'Jason', lastName: 'Bourne', gender: 'male' },
    { firstName: 'Nicky', lastName: 'Parsons', gender: 'female' },
    { firstName: 'Marie', lastName: 'Kreutz', gender: 'female' },
  ];                                          // ^ gender added
 

Much better! But still, we’re stuck. Shall we modify people collection and remove one of the items? What if we wanted to get the whole list back again?

As you might remember from the Approaching Problems note, JavaScript documentation contains a lot of useful information about methods Arrays, Strings and other types support.

The next method we’re going to learn about is filter.

When filter comes in handy

Filtering is an operation that allows us to keep the same number or less items depending on instructions we provide it with.

16ITAqOl0

The image above illustrates the process.

Given a collection of numbers 1, 2 and 3, we’d like to produce a new collection that contains only these items which equal 2. In our example a single-element array is produced. Remember, the function you provide to filter method should return either true or false.

Given our collection of people records:


  var people = [
    { firstName: 'Jason', lastName: 'Bourne', gender: 'male' },
    { firstName: 'Nicky', lastName: 'Parsons', gender: 'female' },
    { firstName: 'Marie', lastName: 'Kreutz', gender: 'female' },
  ];
 

We’d like to write some functions that help us filter items based on the person’s gender. Here comes the first one:


  /**
   * Sample Usage:
   *
   *   alwaysTrue({ gender: 'male' });
   *   // => true
   *
   *   alwaysTrue({ gender: 'female' });
   *   // => true
   *
   */
  function alwaysTrue() {
    return true;
  }
  

Wait, what? What purpose does it serve? Correct, it returns a collection containing the same amount of items, let’s verify that:


  people.filter(alwaysTrue);
  // => 
  // [
  //   { firstName: 'Jason', lastName: 'Bourne', gender: 'male' },
  //   { firstName: 'Nicky', lastName: 'Parsons', gender: 'female' },
  //   { firstName: 'Marie', lastName: 'Kreutz', gender: 'female' },
  // ]

The function above returns an array with an identical number of items input one had. How about writing some more functions?


  /**
   * Sample Usage:
   *
   *   isMale({ gender: 'male' });
   *   // => true
   *
   *   isMale({ gender: 'female' });
   *   // => false
   *
   */
  function isMale(person) {
    return person.gender === 'male';
  }

  /**
   * Sample Usage:
   *
   *   isFemale({ gender: 'male' });
   *   // => false
   *
   *   isFemale({ gender: 'female' });
   *   // => true
   *
   */
  function isFemale(person) {
    return person.gender === 'female';
  }
  

Time to test them out:


  people.filter(isMale);
  // => 
  // [
  //   { firstName: 'Jason', lastName: 'Bourne', gender: 'male' },
  // ]

  people.filter(isFemale);
  // => 
  // [
  //   { firstName: 'Nicky', lastName: 'Parsons', gender: 'female' },
  //   { firstName: 'Marie', lastName: 'Kreutz', gender: 'female' },
  // ]
  

Pretty neat, huh? How about an edge case where none of the items would match?


  /**
   * Sample Usage:
   *
   *   alwaysFalse({ gender: 'male' });
   *   // => false
   *
   *   alwaysFalse({ gender: 'female' });
   *   // => false
   *
   */
  function alwaysFalse() {
    return false;
  }

And the results:


  people.filter(alwaysFalse);
  // => [ ]
  

Phew! At least we've got an array back!

Avoiding unnecessary computations

Since the map and filter operations always return an array as the result, we could chain the operations.

Once again, here’s our fullName function definition for reference:


  /**
   * Sample Usage:
   *
   *   fullName({ firstName: 'Jason', lastName: 'Bourne' });
   *   // => 'Jason Bourne'
   *
   */
  function fullName(person) {
    return person.firstName + ' ' + person.lastName;
  }

Example: Take a list of people, compute their full names and show only females.

  
  // BAD! Will return an empty array, do you know why?
  people.map(fullName).filter(isFemale);
  // => [ ]

Can you spot the glitch above?

The reason an empty array is returned is simply because when we transform an array of person records into their full names, we lose an information about their gender thus none of them matches.

This leads to another thought - why did we compute full name for male records? This looks like an unnecessary step. Well spotted!

The resolution is quite simple - filter first, then map!


  people.filter(isFemale).map(fullName);
  // => [ 'Nicky Parsons', 'Marie Kreutz' ]
  

And the full code follows:

  
  var people = [
    { firstName: 'Jason', lastName: 'Bourne', gender: 'male' },
    { firstName: 'Nicky', lastName: 'Parsons', gender: 'female' },
    { firstName: 'Marie', lastName: 'Kreutz', gender: 'female' },
  ];

  /**
   * Sample Usage:
   *
   *   fullName({ firstName: 'Jason', lastName: 'Bourne' });
   *   // => 'Jason Bourne'
   *
   */
  function fullName(person) {
    return person.firstName + ' ' + person.lastName;
  }

  /**
   * Sample Usage:
   *
   *   alwaysTrue({ gender: 'male' });
   *   // => true
   *
   *   alwaysTrue({ gender: 'female' });
   *   // => true
   *
   */
  function alwaysTrue() {
    return true;
  }

  /**
   * Sample Usage:
   *
   *   alwaysFalse({ gender: 'male' });
   *   // => false
   *
   *   alwaysFalse({ gender: 'female' });
   *   // => false
   *
   */
  function alwaysFalse() {
    return false;
  }

  /**
   * Sample Usage:
   *
   *   isMale({ gender: 'male' });
   *   // => true
   *
   *   isMale({ gender: 'female' });
   *   // => false
   *
   */
  function isMale(person) {
    return person.gender === 'male';
  }

  /**
   * Sample Usage:
   *
   *   isFemale({ gender: 'male' });
   *   // => false
   *
   *   isFemale({ gender: 'female' });
   *   // => true
   *
   */
  function isFemale(person) {
    return person.gender === 'female';
  }

  people.filter(alwaysTrue);
  // =>
  // [
  //   { firstName: 'Jason', lastName: 'Bourne', gender: 'male' },
  //   { firstName: 'Nicky', lastName: 'Parsons', gender: 'female' },
  //   { firstName: 'Marie', lastName: 'Kreutz', gender: 'female' },
  // ]

  people.filter(alwaysFalse);
  // => [ ]

  people.filter(isMale);
  // =>
  // [
  //   { firstName: 'Jason', lastName: 'Bourne', gender: 'male' },
  // ]

  people.filter(isFemale);
  // =>
  // [
  //   { firstName: 'Nicky', lastName: 'Parsons', gender: 'female' },
  //   { firstName: 'Marie', lastName: 'Kreutz', gender: 'female' },
  // ]

  // We lose information about gender after mapping to full name
  people.map(fullName).filter(isFemale);
  // => [ ]

  people.filter(isFemale).map(fullName);
  // => [ 'Nicky Parsons', 'Marie Kreutz' ]
 

Another lesson learned! Good luck experimenting!