Skip to content

Instantly share code, notes, and snippets.

@the-vampiire
Last active September 21, 2020 21:11
Show Gist options
  • Save the-vampiire/5090080c3909c217d5ca361fd2e31777 to your computer and use it in GitHub Desktop.
Save the-vampiire/5090080c3909c217d5ca361fd2e31777 to your computer and use it in GitHub Desktop.
iterative array method notes

Array Methods Outline

What You Should Learn

At the end of this section you should feel comfortable with the following concepts:

  • how split() and join() can be used to operate on Strings as Arrays and vice-versa
  • how slice() can be used to create slices, or copies, of an original Array
    • for making copies: slice(0)
    • for getting copies of element subsets from the original Array: slice(startIndex, endIndex)
  • the differences between imperative and declarative programming
  • how JavaScript's declarative method and property names can help us understand what code does without necessarily understanding how it works
  • what an iterative method is
  • what a function signature is and how it can be used to describe and understand functions in a general way
  • how map() can be used to copy and transform the elements in an Array
  • how filter() can be used to filter out elements of an original Array
  • what the accumulator pattern is and how it works
  • how reduce() can be used to apply the accumulator pattern on an Array
  • how method chaining works
  • how reduce() can replace multiple chained Array method calls to improve efficiency

Helper Array Methods

split() and join()

These two methods represent the conversion between a String <---> Array of sub strings. The sub strings are either smaller parts of the original String (when calling split()) or are put together into one larger String (when calling join()).

Think of it like a physical string. When you start with a string you can split it into smaller pieces. But these are all hard to manage individually so you put them in a box [Array]. If you already have a box of smaller strings you can join them back together into a new string.

Let's extend this analogy to the argument of both methods, the delimiter.

If you start with a string you need to identify where along the string to split its segments. So you grab a marker and slash some lines along the string to guide you when splitting. These markings serve the same purpose as the delimiter. They tell the split() method where it should split the String, everything leading up to that point gets turned into a smaller String and added as an element to the returned Array.

split()

Split is a String method that splits a String into an Array of smaller Strings. Split takes one optional argument, a delimiter, that is used to determine what locations in the original String should be split up. Every character in the String to the left of the delimiter will be saved as an element of the returned Array. split(delimiter):

  • delimiter: the character to split the String by
    • common examples include a space ' ' and a comma , or both ,
    • the delimiter is the String that you consider to be separating the smaller strings you want
  • splits the String at each match of the delimiter into an Array of smaller Strings
  • examples:
const spaces = 'this will be split by spaces';
const spacesDelimiter = ' '; // split at each space
const spacesSplit = spaces.split(spacesDelimiter);
console.log(spacesSplit); // ['this', 'will', 'be', 'split', 'by', 'spaces']

const commas = 'each,one,of,these,is,split';
const commasDelimiter = ','; // split at each comma
const commasSplit = commas.split(commasDelimiter);
console.log(commasSplit); // ['each', 'one', 'of', 'these', 'is', 'split']

const commaAndSpaces = 'each, one, of, these, is, split';
const commaAndSpacesDelimiter = ', '; // split at each comma+space
const commasAndSpacesSplit = commasAndSpaces.split(commaAndSpacesDelimiter);
console.log(commasAndSpacesSplit); // ['each', 'one', 'of', 'these', 'is', 'split']

join()

Join is the reciprocal Array method that turns the split Array back into a String. Here we pass the delimiter again but it is used to stitch the String elements back into one single String. Sometimes the use of the delimiter when joining is referred to as the glue because it glues the pieces back together with the glue String in between. join(delimiter):

  • delimiter: the character to join each String element to the next
    • common examples include a space ' ' and a comma , or both ,
    • the delimiter is the String that you want to put between each of the String elements when they are joined into a single String
  • examples:
const spaces = 'this will be split by spaces';
const spacesDelimiter = ' '; // split at each space
const spacesSplit = spaces.split(spacesDelimiter);
console.log(spacesSplit); // ['this', 'will', 'be', 'split', 'by', 'spaces']

const commas = 'each,one,of,these,is,split';
const commasDelimiter = ','; // split at each comma
const commasSplit = commas.split(commasDelimiter);
console.log(commasSplit); // ['each', 'one', 'of', 'these', 'is', 'split']

const commaAndSpaces = 'each, one, of, these, is, split';
const commaAndSpacesDelimiter = ', '; // split at each comma+space
const commasAndSpacesSplit = commasAndSpaces.split(commaAndSpacesDelimiter);
console.log(commasAndSpacesSplit); // ['each', 'one', 'of', 'these', 'is', 'split']

const sample = 'abcde';
const emptyDelimiter = ''; // empty string delimiter

// splitting with an empty string will return individual characters
// can you understand why?
const individualCharacters = sample.split(emptyDelimiter);
console.log(individualCharacters); // ['a', 'b', 'c', 'd', 'e']

slice()

Slice lets us slice an Array into a copy of equal or smaller size. We can slice an Array by providing starting and (optional) ending indices. The method will return a copy of the Array with the elements between the two indices.

  • slice(startIndex, endIndex)
    • startIndex: the 0-based index that you want to begin copying from
    • endIndex: the 0-based index that you want to stop copying at
      • if no endIndex is provided then the slice will continue until the end of the Array
      • note: the endIndex is non-inclusive meaning it will go up to but not include the element at that index
  • copy the entire Array: slice(0)
    • begins at 0 and goes to the end of the Array since no endIndex is provided
const arr = [1, 2, 3];
console.log(arr.slice(0)); // [1, 2, 3]
console.log(arr.slice(1)); // [2, 3], "slices" from the 1st index [2] to the end
console.log(arr.slice(0, 1)); // [1], "slices" from the 0th index [1] up to but not including the 1st index [2]
console.log(arr.slice(0, 2)); // [1, 2], "slices" from the 0th [1] up to but not including the 2nd index [3]

Iterative Array Methods

Recall that Arrays, like most other types in JavaScript, are objects - in particular data structure objects. As a data structure they serve to hold objects of any kind (including other Arrays!) in an ordered sequence. We use the name element to describe the objects in the Array in a general way.

Any work we want to do across all the elements in an Array can be done using a classic for(;;) {...} or while() {...} loop. In cases of extraordinarily large data sets these may even be more performant than using an array method.

But when dealing with more commonly sized data sets this approach to programming is both tedious and verbose. Each invidual aspect of the loop and loop-block behavior needs to be written explicitly. As a consequence the the development time is prolonged.

Imperative and Declarative Programming

This style of programming is an example of imperative programming, where each step is dictated explicitly. Some developers, especially beginners, prefer imperative programming. They accept the tradeoff of verbosity for the linear readability of the code.

There is another style of programming called declarative programming. In declarative programming explicit naming of variables and verb-named methods and functions are composed to make the code read like a logical story rather than a step-by-step list of granular details.

The debate between both approaches is one you should research and decide on for yourself. I prefer a mostly declarative approach and turn to imperative coding in complex cases where finer details are worth stating explicitly.

In this course most if not all of the examples will be written in a declarative way. There is no right or wrong path that is just the approach I enjoy coding and teaching by.

More reading:


JavaScript provides several declarative Array methods that make common Array operations involving looping and operating over elements light on syntax and easy to use.

Array methods are accessed like any other object method - using the dot notation.

// directly on an array object
[1, 2, 3].arrayMethodName(callbackFunction); 

const arr = [1, 2, 3];
// on a variable assigned to an array
arr.arrayMethodName(callbackFunction); 

const objectName = {
  propertyName: [1, 2, 3],
};
// on a property of an object assigned to an array
objectName.propertyName.arrayMethodName(callbackFunction); 

All of the methods accept a callback function which determines the operations performed on each element. These operations are driven by what each method expects to be returned from the callback (detailed below).

This callback function can be written inline using an arrow function or defined externally.

// inline
[].arrayMethodName(() => {});

// defined externally
const callbackFunction = () => {};
[].arrayMethodName(callbackFunction);

Each method will iterate (loop) over the elements in the array. For each iteration the callback function is executed by passing the function arguments specific to that iteration.

Callback Function Signatures

A function signature is a term used to describe the parameters and return type of a function (or method). It is most often used in statically typed languages to support a concept called method overloading. In JavaScript, a dynamically typed language, we don't have method overloading but the term is still useful to describe functions at a high level.

Most of the methods use this function signature: (element, index, array): return specific to each Array method (detailed below)

Lets take a look at each parameter and their frequency of use:

  • [often] element: the current element of the iteration
  • [occasionally] index: the index of the current iteration
  • [rare/never] array: the array the method was called on

If there are unused parameters you don't need to define them in your callback function. Any undefined parameters will just be ignored. Most often you will work with just the element:

// note for multi-line arrow-functions you should use parenthesis and brackets
const callbackFunction = (element) => {
  // for multi-line callbacks
};

// for simple return-only callbacks no parenthesis or brackets are needed
const callbackFunction = element => /* simple return statement using element */;

// inline
[].arrayMethodName(element => { /* simple or multi-line */ })

Here are the methods that use this function signature for their callbacks. Notice that each method has its own expectation of what the callback should return:

  • map():
    • return a transformed element
  • filter():
    • return a boolean
      • true: keep this element in the filtered list
      • false: skip and go to the next element

Here are the methods that do not use this signature. These are covered in greater detail later in the notes:

  • sort()
  • reduce()

The Methods in Detail

Our goal here is to filter the initialArray to only contain even numbers. Lets introduce a common use case with our first, declaratively named method - filter().

filter()

  • usage: often (until you get comfortable with reduce)
  • purpose: iterates over elements and filters according to a boolean rule
  • returns: a new array with only the elements that passed the filter

Imperative Approach

const initialArray = [1, 2, 3, 4];
// this array will hold all the filtered elements we keep
const filteredArray = [];

// our loop to iterate over the indices of the array
for (let index = 0; index < initialArray.length; ++index) {
  // access the current element of the iteration using the current index
  const element = initialArray[index];

  // store the boolean result of our condition
  const isEven = element % 2 === 0;

  // copy the element to the filtered Array if it is even (true)
  if (isEven) filteredArray.push(element);
}

console.log(initialArray); // [1, 2, 3, 4]
console.log(filteredArray); // [2, 4]

Declarative Approach

// traditional function
function isEvenCallback(
  element, // current element of the iteration
  index, // current index of the iteration
  array, // array being iterated over
) {
  // return the boolean result of our condition
  return element % 2 === 0;
};

// arrow function
const isEvenCallback = (element, index, array) => element % 2 === 0;

// since we only use the 'element' parameter lets condense it further
const isEvenCallback = element => element % 2 === 0;

const initialArray = [1, 2, 3, 4];
const filteredArray = initialArray.filter(isEvenCallback);

// heres how it looks if it were defined inline
// const filteredArray = initialArray.filter(element => element % 2 === 0);

console.log(initialArray); // [1, 2, 3, 4]
console.log(filteredArray); // [2, 4]

.map()

  • usage: often
  • purpose: loops over an array and applies a transformation to each element.
  • returns: a new array with the transformed elements

Map is without a doubt the most used Array method especially in backend development. It allows us to take an Array and transform it into a new Array (returned by map()). We transform the Array by applying a transformation function (the callback to map()) to each element in the Array.

In simple terms: map() will return a new Array. This array will have the elements returned by each callback function call. So whatever you return from the callback will be the transformed element that is saved in the Array returned by map()

The transformation function expects that you return the transformed element. You can do any logic you want in the function body and finally at the end return the transformed element. Just like with other simple cases if our function has no block content besides a return statement we can write a one-line arrow function.

Let's start with a simple example. Say we have a list of numbers and we want to transform it by doubling every number.

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

// our transformation function
// each number [element] is multiplied by 2 and returned
// this means that each element is "transformed" into the number * 2
const doubleNumberCallback = number => number * 2;
const doubledNumbers = numbers.map(doubleNumberCallback);
console.log(doubledNumbers); // [2, 4, 6, 8]

// or inline
// const doubleNumbers = numbers.map(number => number * 2);

What about a slightly more complex example. Say we have a list of user objects with a bunch of properties. But we want a simple list that just has their fullName string values. We can use map to create a new list of just those names.

const users = [
  { fullName: 'Patrick Vamp', hasDogs: true, hasCats: false },
  { fullName: 'Elon MUsk', hasDogs: false, hasCats: true },
  { fullName: 'John Stamos', hasDogs: false, hasCats: false },
];

// here each element has been renamed 'user' to be more explicit
// we return just the user's 'username' property value
const names = users.map(user => user.fullName);
console.log(names); // ['Patrick Vamp', 'Elon MUsk', 'John Stamos']

You will find that map is equally useful for "shaping" objects. The "shape" of an object describes the properties and value types of those properties within an object. Say our user example above.

The shape of a user is { fullName: String, hasDogs: Boolean, hasCats: Boolean }. Using map we are able to transform or re-shape our objects. In the above case we converted them into simple fullName strings. But what about the below case where we want to re-shape our user objects to split their fullName into firstName and lastName properties?

const users = [
  { fullName: 'Patrick Vamp', hasDogs: true, hasCats: false },
  { fullName: 'Elon MUsk', hasDogs: false, hasCats: true },
  { fullName: 'John Stamos', hasDogs: false, hasCats: false },
];

const splitNameUsers = users.map((user) => {
  // splits the string at each blank ' ' space into an array of elements
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split
  const splitFullName = user.fullName.split(' '); // ['Patrick', 'Vamp']
  const firstName = splitFullName[0]; // first element
  const lastName = splitFullName[1]; // second element

  const newShape = {
    firstName, // shorthand syntax, same as firstName: firstName
    lastName,
    hasDogs: user.hasDogs,
    hasCats: user.hasCats,
  };

  return newShape;
});

console.log(splitNameUsers); // [{ firstName: 'Patrick', lastName: 'Vamp', hasDogs: true, hasCats: false }, ...]

.reduce()

  • usage: occasionally
  • purpose: loops over an array and reduces the elements into a single element
    • an application of the accumulator pattern
  • returns the reduced item, one of several possibilities:
    • plain object - {}
    • Array - []
    • String - ""
    • Number - 1

The Accumulator Pattern

The accumulator pattern is a way of building up or accumulating data. The simplest example of the accumulator is a loop over an Array of numbers to sum their values. Here we are reducing an Array of number elements into a single number element.

Let's look at it from the imperative approach first.

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

let sum = 0;
for (let index = 0; index < numbers.length; ++index) {
  const number = numbers[index];
  sum = sum + number;
}

console.log(sum);

What if the loop-block were turned into a function? It takes in a number and the previous sum, and returns the new sum. In other words it receives an accumulator element and the next element in the array and returns the new accumulator element

const getNewSum = (previousSum, currentElement) => {
  return previousSum + currentElement;
}

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

let sum = 0;
for (let index = 0; index < numbers.length; ++index) {
  const number = numbers[index];
  sum = getNewSum(sum, number);
}

console.log(sum);

What if we turned the entire operation, both the loop and its inner block, into a function. What sort of arguments would it take? Think about the two variables that were defined outside the loop. One is an Array of number elements, and the other is an initial value for the accumulator (our sum).

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

const getNewSum = (previousSum, currentElement) => {
  return previousSum + currentElement;
}

const reduceSum = (arr, accumulatorCallback, initialValue) => {
  for (let index = 0; index < numbers.length; ++index) {
    const number = numbers[index];
    initialValue = accumulatorCallback(initialValue, number);
  }

  return initialValue;
}

let sum = 0;
const finalSum = reduceSum(numbers, getNewSum, sum);
console.log(finalSum);

With this final definition we are close to how the actual reduce() method functions. One major difference is it receives an Array passed as an argument whereas the reduce() method is called on an Array like numbers.reduce(...).

Let's take a look at what the function is doing. It receivs an Array to iterate over. It declares an accumulator variable (reassignable by let) and assigns it an initial value, in this case 0 for our sum accumulator. It then iterates over the Array and reassigns the previous sum to a new value derived from the previous sum and current element.

In other words it reassigns the accumulator value to what is returned by the callback function,getNewSum(). In this case we have defined the callback as adding the previous sum to the current number. But we could have easily defined it to have any other instruction.

All our reducer, reduceSum() is interested in is an Array of values and a callback that defines how accumulation should be evaluated.

As proof let's show how easy it is to swap in a new callback function that now calculates the product of the numbers in the Array. While also using the same reducer function to calculate the sum as we had before.

We'll leave all of the other logic the same besides generalizing our naming to get us closer to understanding reduce().

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

const getNewSum = (previousSum, currentElement) => {
  return previousSum * currentElement;
}

const getNewProduct = (previousProduct, currentElement) => {
  return previousProduct * currentElement;
}

const reduceArray = (arr, accumulatorCallback, initialValue) => {
  for (let index = 0; index < elements.length; ++index) {
    const element = elements[index];
    initialValue = accumulatorCallback(initialValue, element);
  }

  return initialValue;
}

let initialProduct = 0;
const totalProduct = reduceArray(elements, getNewProduct, initialProduct);
console.log('product: ', totalProduct);

let initialSum = 0;
const totalSum = reduceArray(elements, getNewSum, initialSum);
console.log('sum: ', totalSum);

It should now be clear what reduce()'s purpose is: iterate over an Array and reduce its elements into a single element. In these cases we reduce an Array of numbers into a product and sum (number element). But we will see many more examples down the road that increase in complexity but provide an equal reduction of code.

Two key differences between our example and reduce() itself are the arr and initialValue parameters. When using our reduce() method we call it on the Array and the initial value is provided as an argument. However, the initial value does not get defined and assigned outside, you simply provide the value and the method takes care of managing assignment and reassignment while iterating. This helps us avoid external mutations of variables which is an anti-pattern.

Let's finally explore how the reduce() method works from its function signature to callback function. Reduce has a unique signature out of all the Array methods, it receives both a callback and initial value

  • signature: (callbackFunction, initialValue)
  • callbackFunction
    • signature: (previousAccumulatorValue, currentElement)
      • often abbreviated (prev, curr), (prevAcc, element), or (acc, e)
    • return: the new value of the accumulator
[].reduce(
  (previousAccumulatorValue, currentElement), // callbackFunction
  0, // initialValue 
);

// using our sum and product callbacks
const getNewSum = (previousSum, currentElement) => {
  return previousSum * currentElement;
}

const getNewProduct = (previousProduct, currentElement) => {
  return previousProduct * currentElement;
}

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

const sum = numbers.reduce(getNewSum, 0);
console.log(sum);

const product = numbers.reduce(getNewProduct, 0);
console.log(product);

// if you want to start at another number just pass it as the second argument
const productFromTen = numbers.reduce(getNewProduct, 10);
console.log(productFromTen);

Now that you've seen where reduce comes from and how it works conceptually let's see some more interesting use cases.

filtering

const filterEvens = (evenNumbers, number) => {
  if (number % 2 === 0) {
    evenNumbers.push(number);
    return evenNumbers;
  }

  return evenNumbers;
}

const numbers = [1, 2, 3, 4];
const onlyEvens = numbers.reduce(filterEvens, []);
console.log(onlyEvens);

Notice in this case that our accumulator is an Array rather than a number. To accumulate our Array we conditionally push new values into it. After pushing the even number into the evenNumbers we return this new accumulator value. In the odd case we simply return the current evenNumbers value since the number is not even (meaning it does not get accumulated).

However, it is a bad practice to mutate arguments or anything defined external to a function. This can lead to issues with external variables having unexpected values when they are used later in the code (because their reassignment is hidden in a function).

Instead it is best to make copies of arguments and operate on those. This is another reason Array methods are so useful - besides sort() and splice all of the methods return a copied Array, never affecting the original.

An easy way to avoid mutating the accumulator argument is to use the spread syntax of Arrays. Here we return a new Array each time which contains all the previously accumulated elements (through spreading), and the new element. The spread syntax ...Array spreads out all of the elements in the Array into the new Array surrounding it. By spreading we never change the value of the original Array but still retain our accumulator pattern.

This is just one of many uses of the spread operator. We will explore some of them later but if you want to learn more now here is a good place to start.

const filterEvens = (evenNumbers, number) => {
  // event, return the accumulator with the current even number
  if (number % 2 === 0) return [...evenNumbers, number];
  // not even, keep the current accumulator value and go to the next iteration
  return evenNumbers;
}

Let's look at reducing into an Object. The example is for a frequency counter of characters in a String. We will create an object with character: count properties to store our character frequencies.

const accumulateFrequency = (frequencies, character) => {
  const existingCount = frequencies[character]; // access the current character property in the frequencies object

  // if the character is already a property of the object this will be a count number
  if (existingCount) {
    frequencies[character] = existingCount + 1;
  } else { // if it is undefined then give it an initial count of 1
    frequencies[character] = 1;
  }

  // return the accumulated object
  return frequencies;
};

const string = 'what are the frequencies of these characters?';
const characters = string.split(''); // split into an Array of individual String character elements

const characterFrequencies = characters.reduce(accumulateFrequency, {});
console.log(characterFrequencies);

Method Chaining

For the final example let's see how reduce() can replace both a map() and filter() call. Our goal here is to multiply each number by 5 (map) and filter only the even results:

First let's look at how we could accomplish this by using map() and filter() chained onto each other. Method chaining is a way of calling one method "on" another by using the dot operator. This work so long as:

  • what is to the left of the . is the correct type expected by
    • this can be a base value
    • this can be an expression that is evaluated to return a base value
  • the method call to the right of the .

For example, say we call map() on an array: [].map.

To the left of the . is an Array type [] and to the right is an Array method map() that expects an Array to operate. This is valid syntax since both the left and right side of the . are of the correct types.

What if we called, or chained, filter() onto the map() call? map() returns an array (left of the .) and filter() expects an Array on the right of the . syntax.

This may seem confusing it will help to recall how JavaScript evaluates expressions.

First what is the statement in this case?

const mappedAndFiltered = [].map(mapCallback).filter(filterCallback);

Everything going on the right side of the assignment (=) as our expression. JavaScript will evaluate the expression from left to right until it reaches the end of the chain and returns that value.

An easy way to understand this is to break it apart into steps. Notice that after each step is evaluated whatever it returns is given to the next step. If we broke this apart into the evaluation steps this is what it looks like:

  • statement: const mappedAndFiltered = [].map(mapCallback).filter(filterCallback);
  • expression to evaluate (evaluation chain): [].map(mapCallback).filter(filterCallback);
  • step 1: [] this is a base Array value, nothing to evaluate, firstArray
  • step 2: firstArray.map() -> call map() on the Array to the left -> evaluate the map() method call -> returns an Array [], mappedArray
  • step 3: mappedArray.filter() -> call filter() on the Array returned by the previous step -> evaluate the filter() method call -> returns an Array -> [], finalArray since it is the last step in the evaluation chain.
  • step 4: the expression has been evaluated into an Array finalArray and is assigned to the variable mappedAndFiltered

So during each step of evaluation the method calls are converted into their return type (an Array). If we go from left to right we can see that each previous step returns the expected type (Array) needed by the next method call step.

Here is an example of how to reverse a String (a classic interview teaser). Notice that we are blending both String and Array methods in the chain. This may seem incorrect but remember each method is receiving exactly the type it expects.

We will chain 3 methods together that will: split the String into an array, reverse the elements of that Array, and finally join that (reversed) Array back together to make the reversed String:

const start = 'abcdefg';
const reversed = start.split('').reverse().join('');
console.log(reversed); // 'gfedcba'

Spread across multiple lines for readability:

const start = 'abcdefg';
const reversed = start
  .split('') // splits the String into an Array of invidiual character elements ['a', 'b', 'c', ...]
  .reverse() // reverse the order of the Array's elements ['g', 'f', 'e', ...]
  .join(''); // join the String elements of the Array back into a String

console.log(reversed); // 'gfedcba'

Let's break down the evaluation steps again to see why this works:

  • expression: start.split('').reverse().join('');
  • step 1: start this is a base String value, nothing to evaluate
  • step 2: split('') call on the String value -> evaluate the method call -> returns an Array of String characters
  • step 3: reverse() called on the Array returned by split() -> evaluate method call -> returns a reversed Array
  • step 4: join() called on the Array returned by reverse() -> evaluate method call -> returns a String made up of the reversed String characters
  • step 5: assign the reversed String to reversed

Now that we understand chaining let's see how it can be used to accomplish our goal:

const numbers = [1, 2, 3, 4];
const mappedAndFiltered = numbers.map(number => number * 5).filter(number => number % 2 === 0);

Let's split this into multiple lines to add some context. This is a common way of writing chained method calls so that each call takes up its own line and we can read it in order of evaluation. Note that JavaScript ignores the white space between each call.

const numbers = [1, 2, 3, 4];
const mappedAndFiltered = numbers // firstArray: [1, 2, 3, 4]
  .map(number => number * 5) // mappedArray: [5, 10, 15, 20]
  .filter(number => number % 2 === 0); // finalArray: [10, 20]

console.log(mappedAndFiltered); // [10, 20]

Because the filteredArray is the last returned value from the chain it becomes the value assigned to mappedAndFiltered.

Some people prefer to use chaining for readability. It reads pretty clearly, take some numbers and map (transform) them by the product of 5 then filter the result. However, consider the impact of large arrays. Remember that each iterative Array method iterates over the entire Array. So if we had an Array of length 10,000 elements. That means we have performed 10,000 iterations for the map() call and another 10,000 iterations for the filter() call. That's 20,000 iterations!

What if we were able to both transform and filter within one iteration? We would cut our processing time tremendously by removing the need for a second complete iteration. Enter our friend reduce() who, like the other iterative Array methods, iterates over the given Array. Using reduce() we can accomplish the same result but only using one iteration over the Aray.

// finalArray is our accumulator
// number is the current number element from the numbers array
const mapAndFilterCallback = (finalArray, number) => {
  const transformedNumber = number * 5;
  const isEven = transformedNumber % 2 === 0;

  // if it is even (passes our filter)
  // spread the previous elements and add the transformedNumber
  if (isEven) return [...finalArray, transformedNumber]; 

  // otherwise if it does not pass our filter
  // ignore (filter) this transformedNumber by just returning the finalArray
  return finalArray; 
};

const numbers = [1, 2, 3, 4];
const mappedAndFiltered = numbers.reduce(mapAndFilterCallback);

console.log(mappedAndFiltered); // [10, 20]

Get creative an try reducing some Arrays into different accumulator types. Remember that all the reducer cares about is a callback returning a new accumulator value and an initial accumulator object. It is entirely up to you what you define as accumulation and how you will store it as an accumulator.

Practice Challenges

Warmup: Gobbos!

Warmup 1 filter()

You have a small gathering of goblins gathered at your feet. You don't mind the nice ones but the mean ones are rude and keep nipping at your toes. You have to separate the gobbos into two groups, nice and mean. The nice ones are cute and get to stay but the mean ones get punted into the swamp!

Fortunately (unfortunately?) you don't have real gobbos at your toes. You have been given an initial list of gobbos with a mix of nice and mean. Your challenge is to separate the list into two new lists: niceGobbos and meanGobbos.

const gobbos = [
  { mean: true },
  { mean: false },
  { mean: false },
  { mean: false },
  { mean: false },
  { mean: true },
  { mean: false },
  { mean: false },
  { mean: false },
  { mean: false },
  { mean: false },
  { mean: true },
  { mean: true },
];

const niceGobbos = // your code here
const meanGobbos = // your code here

console.log('niceGobbos: ', niceGobbos);
console.log('meanGobbos: ', meanGobbos);

Warmup 2 map()

A powerful wizard has frozen you mid-punt of the cowering gobbo. The wizard offers to teach you a spell that will transform the mean gobbos into nice ones. This should be easy since you have already separated them.

Your challenge is to write the spell! Fill in the nicifyGobbo function below and use it to transform all the mean gobbos.

// use the meanGobbos list from problem 1

const nicifyGobbo = gobbo => // cast the right spell to make that gobbo nice

const nicifiedGobbos = meanGobbos.map(nicifyGobbo);
console.log('nicifiedGobbos: ', nicifiedGobbos);

Real World: Frontend Development

Real World 1 filter()

You are designing a component for a user profile on your frontend. The component needs to show a list of friends separated by three sections: Family, Close Friends, and Other Friends. You are received an Array of friends from an API request to your backend with the following shape:

{
  username: String,
  avatarURL: String,
  isFamily: Boolean,
  isCloseFriend: Boolean,
}

Your challenge is to separate the list of friends into three Arrays according to the rules below:

  • family: only friends that have a isFamily property of true
  • closeFriends: only friends that have a isCloseFriend property of true
  • otherFriends: all other friends that are neither family nor close

Bonus: How could you solve this using reduce()?

  • Hint: Use an object as the accumulator.
    • What properties should you define in the object?
    • What initial values should they have?
    • How will you access and update these properties in your reduce() callback?
const friends = [
  { username: 'Ima38',
    avatarURL: 'https://s3.amazonaws.com/uifaces/faces/twitter/sovesove/128.jpg',
    isFamily: true,
    isCloseFriend: true },
  { username: 'Rodrick_Schuster58',
    avatarURL: 'https://s3.amazonaws.com/uifaces/faces/twitter/alagoon/128.jpg',
    isFamily: true,
    isCloseFriend: false },
  { username: 'Kaela_Reinger93',
    avatarURL: 'https://s3.amazonaws.com/uifaces/faces/twitter/ismail_biltagi/128.jpg',
    isFamily: false,
    isCloseFriend: false },
  { username: 'Jody_Becker83',
    avatarURL: 'https://s3.amazonaws.com/uifaces/faces/twitter/paulfarino/128.jpg',
    isFamily: true,
    isCloseFriend: false },
  { username: 'Verner71',
    avatarURL: 'https://s3.amazonaws.com/uifaces/faces/twitter/madshensel/128.jpg',
    isFamily: true,
    isCloseFriend: false },
  { username: 'Landen.Rippin32',
    avatarURL: 'https://s3.amazonaws.com/uifaces/faces/twitter/theonlyzeke/128.jpg',
    isFamily: true,
    isCloseFriend: true },
  { username: 'Crawford.Rau',
    avatarURL: 'https://s3.amazonaws.com/uifaces/faces/twitter/artvavs/128.jpg',
    isFamily: false,
    isCloseFriend: false },
  { username: 'Brenna11',
    avatarURL: 'https://s3.amazonaws.com/uifaces/faces/twitter/bfrohs/128.jpg',
    isFamily: false,
    isCloseFriend: true },
  { username: 'Guido.Carroll69',
    avatarURL: 'https://s3.amazonaws.com/uifaces/faces/twitter/jeffgolenski/128.jpg',
    isFamily: false,
    isCloseFriend: false },
  { username: 'Santiago_Hills',
    avatarURL: 'https://s3.amazonaws.com/uifaces/faces/twitter/toddrew/128.jpg',
    isFamily: false,
    isCloseFriend: true },
]

// write your solution here

Real World 2 map()

Your user base and their friends lists are growing rapidly. You have decided to refactor the friends component to show more users in the same space. You plan on accomplishing this with just showing their usernames and dropping the avatar images. You receive the same list of friends from your API.

Your challenge is to transform the friends list into a list of just the friend usernames.

// your solution here

Real World 3 map() and filter(), or reduce()

You now want to use the friend sections with the username only approach. You receeive the same list of friends from your API.

Your challenge is to both separate the friends groups (according to the rules in problem 1) as well as transform the friend objects into username Strings. You can accomplish this with either chained map() and filter() calls or a single reduce().

// your solution here

Can you make sense of this script that was used to make the example friends list above? Here we are using a package called faker which helps generate random "mock" data. The purpose of this short script is to create an Array of 10 mock 'friend' objects.

The syntax and some of the functions may be foreign to you. But if you follow the declarative naming and piece together the "story" of this code you should be able to follow the logic.

Think about what each variable and property are called and what each function or method returns. Often just these pieces of information can help you navigate declarative code.

If you don't get it now don't worry! We will be using faker and writing functions and scripts just like this when we get into testing our code.

// this is how we import and access the functions from the faker library
// note if you dont have faker installed this code wont run
const { random: { number }, internet: { userName, avatar } } = require('faker');

// dont worry about the syntax, the name alone should tell you what this function does
const randomBoolean = () => [true, false][number({ max: 1 })];

// look at the name, argument, and what is returned
// remember when methods are chained the last return statement is what gets returned
const makeFriendMocks = count => Array(count) // Array(count) creates an Array of count length
  .fill(null) // fills the empty Array with null values (placeholders for mapping)
  .map( // map only works with elements so we fill it with null before
    // this is the last return statement
    // given the goal of this function and what it returns you can guess
    // what this inline callback function is doing

    () => ({ // notice the callback function doesnt use the 'null' element
      // look at the names of the functions to guess what they might do
      username: userName(),
      avatarURL: avatar(),
      isFamily: randomBoolean(),
      isCloseFriend: randomBoolean(),
    }),
  );

const friends = makeFriendMocks(10);
console.log(friends);
@the-vampiire
Copy link
Author

the-vampiire commented Dec 22, 2018

.map()
.reduce()

MDN is an awesome site for learning javascript. crystal clear examples and documentation of literally everything to do in javascript. If you ever want to learn something new start by searching thing to learn and then js MDN at the end to get MDN articles, like "array reduce MDN"

@the-vampiire
Copy link
Author

the-vampiire commented Dec 22, 2018

this is another great site (javascript.info) that covers everything in detail
here is their article on array methods at the end is a section called 'tasks' that has some example problems to work on. I would at least try the filter and map problems.

@the-vampiire
Copy link
Author

the-vampiire commented Dec 23, 2018

Practice Problems

Warmup

1: filter()

You have a small gathering of goblins gathered at your feet. You don't mind the nice ones but the mean ones are rude and keep nipping at your toes. You have to separate the gobbos into two groups, nice and mean. The nice ones are cute and get to stay but the mean ones get punted into the swamp!

Fortunately (unfortunately?) you don't have real gobbos at your toes. You have been given an initial list of gobbos with a mix of nice and mean. Your challenge is to separate the list into two new lists: niceGobbos and meanGobbos.

const gobbos = [
  { mean: true },
  { mean: false },
  { mean: false },
  { mean: false },
  { mean: false },
  { mean: true },
  { mean: false },
  { mean: false },
  { mean: false },
  { mean: false },
  { mean: false },
  { mean: true },
  { mean: true },
];

const niceGobbos = // your code here
const meanGobbos = // your code here

console.log('niceGobbos: ', niceGobbos);
console.log('meanGobbos: ', meanGobbos);

2: map()

A powerful wizard has frozen you mid-punt of the cowering gobbo. The wizard offers to teach you a spell that will transform the mean gobbos into nice ones. This should be easy since you have already separated them.

Your challenge is to write the spell! Fill in the nicifyGobbo function below and use it to transform all the mean gobbos.

// use the meanGobbos list from problem 1

const nicifyGobbo = gobbo => // cast the right spell to make that gobbo nice

const nicifiedGobbos = meanGobbos.map(nicifyGobbo);
console.log('nicifiedGobbos: ', nicifiedGobbos);

Real World

1: filter()

You are designing a component for a user profile on your frontend. The component needs to show a list of friends separated by three sections: Family, Close Friends, and Other Friends. You are received an Array of friends from an API request to your backend with the following shape:

{
  username: String,
  avatarURL: String,
  isFamily: Boolean,
  isCloseFriend: Boolean,
}

Your challenge is to separate the list of friends into three Arrays according to the rules below:

  • family: only friends that have a isFamily property of true
  • closeFriends: only friends that have a isCloseFriend property of true
  • otherFriends: all other friends that are neither family nor close

Bonus:
You notice that some friends are showing up in both the family and close friends lists. You don't want these friends to be duplicated. You notice this is caused by friends with overlapping isFamily and isCloseFriend true fields. You decide to implement a 4th rule: if a friend is both family and close they should only show up in the family list.

How can you change your code to apply this change?

Bonus:
How could you solve this problem using reduce()?

  • Hint: Use an object as the accumulator.
    • What properties should you define in the object?
    • What initial values should they have?
    • How will you access and update these properties in your reduce() callback?
const friends = [
  { username: 'Ima38',
    avatarURL: 'https://s3.amazonaws.com/uifaces/faces/twitter/sovesove/128.jpg',
    isFamily: true,
    isCloseFriend: true },
  { username: 'Rodrick_Schuster58',
    avatarURL: 'https://s3.amazonaws.com/uifaces/faces/twitter/alagoon/128.jpg',
    isFamily: true,
    isCloseFriend: false },
  { username: 'Kaela_Reinger93',
    avatarURL: 'https://s3.amazonaws.com/uifaces/faces/twitter/ismail_biltagi/128.jpg',
    isFamily: false,
    isCloseFriend: false },
  { username: 'Jody_Becker83',
    avatarURL: 'https://s3.amazonaws.com/uifaces/faces/twitter/paulfarino/128.jpg',
    isFamily: true,
    isCloseFriend: false },
  { username: 'Verner71',
    avatarURL: 'https://s3.amazonaws.com/uifaces/faces/twitter/madshensel/128.jpg',
    isFamily: true,
    isCloseFriend: false },
  { username: 'Landen.Rippin32',
    avatarURL: 'https://s3.amazonaws.com/uifaces/faces/twitter/theonlyzeke/128.jpg',
    isFamily: true,
    isCloseFriend: true },
  { username: 'Crawford.Rau',
    avatarURL: 'https://s3.amazonaws.com/uifaces/faces/twitter/artvavs/128.jpg',
    isFamily: false,
    isCloseFriend: false },
  { username: 'Brenna11',
    avatarURL: 'https://s3.amazonaws.com/uifaces/faces/twitter/bfrohs/128.jpg',
    isFamily: false,
    isCloseFriend: true },
  { username: 'Guido.Carroll69',
    avatarURL: 'https://s3.amazonaws.com/uifaces/faces/twitter/jeffgolenski/128.jpg',
    isFamily: false,
    isCloseFriend: false },
  { username: 'Santiago_Hills',
    avatarURL: 'https://s3.amazonaws.com/uifaces/faces/twitter/toddrew/128.jpg',
    isFamily: false,
    isCloseFriend: true },
]

const family = // your code here
const closeFriends = // your code here
const otherFriends = // your code here

2: map()

Your user base and their friends lists are growing rapidly. You have decided to add an all friends component. You plan on save space by just showing their usernames and dropping the avatar images. You receive the same list of friends from your API.

Your challenge is to transform the friends list into a list of just the friend usernames.

// use the entire friends list from problem 1

const usernames = //

3: map() and filter(), or reduce()

You now want to use the friend sections with the username only approach. You receive the same list of friends from your API.

Your challenge is to both separate the friends groups (according to the rules in problem 1) as well as transform the friend objects into username Strings. You can accomplish this with either chained map() and filter() calls or a single reduce().

const familyUsernames = // your code here
const closeFriendsUsernames = // your code here
const otherFriendsUsernames = // your code here

Can you make sense of this script that was used to make the example friends list above?
Here we are using a package called faker which helps generate random "mock" data. The purpose of this short script is to create an Array of 10 mock 'friend' objects.

The syntax and some of the functions may be foreign to you. But if you follow the declarative naming and piece together the "story" of this code you should be able to follow the logic.

Think about what each variable and property are called and what each function or method returns. Often just these pieces of information can help you navigate declarative code.

If you don't get it now don't worry! We will be using faker and writing functions and scripts just like this when we get into testing our code.

// this is how we import and access the functions from the faker library
// note if you dont have faker installed this code wont run
const { random: { number }, internet: { userName, avatar } } = require('faker');

// dont worry about the syntax, the name alone should tell you what this function does
const randomBoolean = () => [true, false][number({ max: 1 })];

// look at the name, argument, and what is returned
// remember when methods are chained the last return statement is what gets returned
const makeFriendMocks = count => Array(count) // Array(count) creates an Array of count length
  .fill(null) // fills the empty Array with null values (placeholders for mapping)
  .map( // map only works with elements so we fill it with null before
    // this is the last return statement
    // given the goal of this function and what it returns you can guess
    // what this inline callback function is doing

    () => ({ // notice the callback function doesnt use the 'null' element
      // look at the names of the functions to guess what they might do
      username: userName(),
      avatarURL: avatar(),
      isFamily: randomBoolean(),
      isCloseFriend: randomBoolean(),
    }),
  );

const friends = makeFriendMocks(10);
console.log(friends);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment