Iterators

Imagine you had a grocery list and you wanted to know what each item on the list was. You’d have to scan through each row and check for the item. This common task is similar to what we have to do when we want to iterate over, or loop through an array. One tool at our disposal is the for loop. However, we also have access to built-in array methods which make looping easier.

Before going deep into iterators let’s start by prerequisite of iterators which is high-order functions.

Higher-Order Functions

We are often unaware of the number of assumptions we make when we communicate with other people in our native languages. If we told you to “count to three,” we would expect you to say or think the numbers one, two and three. We assumed you would know to start with “one” and end with “three”. With programming, we’re faced with needing to be more explicit with our directions to the computer. Here’s how we might tell the computer to “count to three”:

for (let i = 1; i<=3; i++) {
  console.log(i)
}

 

When we speak to other humans, we share a vocabulary that gives us quick ways to communicate complicated concepts. When we say “bake”, it calls to mind a familiar subroutine— preheating an oven, putting something into an oven for a set amount of time, and finally removing it. This allows us to abstract away a lot of the details and communicate key concepts more concisely. Instead of listing all those details, we can say, “We baked a cake,” and still impart all that meaning to you.

In programming, we can accomplish “abstraction” by writing functions. In addition to allowing us to reuse our code, functions help to make clear, readable programs. If you encountered countToThree() in a program, you might be able to quickly guess what the function did without having to stop and read the function’s body.

We’re also going to learn about another way to add a level of abstraction to our programming: higher-order functions. Higher-order functions are functions that accept other functions as arguments and/or return functions as output. This enables us to build abstractions on other abstractions, just like “We hosted a birthday party” is an abstraction that may build on the abstraction “We made a cake.”

In summary, using more abstraction in our code allows us to write more modular code which is easier to read and debug.

Functions as Data

JavaScript functions behave like any other data type in the language; we can assign functions to variables, and we can reassign them to new variables.

Below, we have an annoyingly long function name that hurts the readability of any code in which it’s used. Let’s pretend this function does important work and needs to be called repeatedly!

const announceThatIAmDoingImportantWork = () => {
    console.log(“I’m doing very important work!”);
};

 

What if we wanted to rename this function without sacrificing the source code? We can re-assign the function to a variable with a suitably short name:

const busy = announceThatIAmDoingImportantWork;

busy(); // This function call barely takes any space!

 

busy is a variable that holds a reference to our original function. If we could look up the address in memory of busy and the address in memory of announceThatIAmDoingImportantWork they would point to the same place. Our new busy() function can be invoked with parentheses as if that was the name we originally gave our function.

Notice how we assign announceThatIAmDoingImportantWork without parentheses as the value to the busy variable. We want to assign the value of the function itself, not the value it returns when invoked.

In JavaScript, functions are first class objects. This means that, like other objects you’ve encountered, JavaScript functions can have properties and methods.

Since functions are a type of object, they have properties such as .length and .name and methods such as .toString(). You can see more about the methods and properties of functions in the documentation.

Functions are special because we can invoke them, but we can still treat them like any other type of data. Let’s get some practice doing that!

Functions as Parameters

Since functions can behave like any other type of data in JavaScript, it might not surprise you to learn that we can also pass functions (into other functions) as parameters. A higher-order function is a function that either accepts functions as parameters, returns a function, or both! We call the functions that get passed in as parameters and invoked callback functions because they get called during the execution of the higher-order function.

When we pass a function as an argument to another function, we don’t invoke it. Invoking the function would evaluate to the return value of that function call. With callbacks, we pass in the function itself by typing the function name without the parentheses (that would evaluate to the result of calling the function):

const timeFuncRuntime = funcParameter => {
  let t1 = Date.now();
  funcParameter();
  let t2 = Date.now();
  return t2 – t1;
}

const addOneToOne = () => 1 + 1;

timeFuncRuntime(addOneToOne);

 

We wrote a higher-order function, timeFuncRuntime(). It takes in a function as an argument, saves a starting time, invokes the callback function, records the time after the function was called, and returns the time the function took to run by subtracting the starting time from the ending time.

This higher-order function could be used with any callback function which makes it a potentially powerful piece of code.

We then invoked timeFuncRuntime() first with the addOneToOne() function – note how we passed in addOneToOne and did not invoke it.

timeFuncRuntime(() => {
  for (let i = 10; i>0; i–){
    console.log(i);
  }
});

 

In this example, we invoked timeFuncRuntime() with an anonymous function that counts backwards from 10. Anonymous functions can be arguments too!

Iterator Methods 

The built-in JavaScript array methods that help us iterate are called iteration methods, at times referred to as iterators. Iterators are methods called on arrays to manipulate elements and return values.

In this section, you will learn the syntax for these methods, their return values, how to use the documentation to understand them, and how to choose the right iterator method for a given task.

The .forEach() Method

The first iteration method that we’re going to learn is .forEach(). Aptly named, .forEach() will execute the same code for each element of an array.

The code above will log a nicely formatted list of the groceries to the console. Let’s explore the syntax of invoking .forEach().

  • groceries.forEach() calls the forEachmethod on the groceries array.
  • .forEach() takes an argument of callback function. Remember, a callback function is a function passed as an argument into another function.
  • .forEach() loops through the array and executes the callback function for each element. During each execution, the current element is passed as an argument to the callback function.
  • The return value for .forEach() will always be undefined.

Another way to pass a callback for .forEach() is to use arrow function syntax.

groceries.forEach(groceryItem => console.log(groceryItem));
We can also define a function beforehand to be used as the callback function.
function printGrocery(element){
  console.log(element);
}

groceries.forEach(printGrocery);

The above example uses a function declaration but you can also use a function expression or arrow function as well.

All three code snippets do the same thing. In each array iteration method, we can use any of the three examples to supply a callback function as an argument to the iterator. It’s good to be aware of the different ways to pass in callback functions as arguments in iterators because developers have different stylistic preferences. Nonetheless, due to the strong adoption of ES6, we will be using arrow function syntax.

The .map() Method

The second iterator we’re going to cover is .map(). When .map() is called on an array, it takes an argument of a callback function and returns a new array! Take a look at an example of calling .map():

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

const bigNumbers = numbers.map(number => {
  return number * 10;
});

 

.map() works in a similar manner to .forEach()— the major difference is that .map() returns a new array.

In the example above:

  • numbers is an array of numbers.
  • bigNumbers will store the return value of calling .map() on numbers.
  • numbers.map will iterate through each element in the numbers array and pass the element into the callback function.
  • return number * 10 is the code we wish to execute upon each element in the array. This will save each value from the numbers array, multiplied by 10, to a new array.

If we take a look at numbers and bigNumbers:

console.log(numbers); // Output: [1, 2, 3, 4, 5]
console.log(bigNumbers); // Output: [10, 20, 30, 40, 50]

 

Notice that the elements in numbers were not altered and bigNumbers is a new array.

The .filter() Method

Another useful iterator method is .filter(). Like .map(), .filter() returns a new array. However, .filter() returns an array of elements after filtering out certain elements from the original array. The callback function for the .filter() method should return true or false depending on the element that is passed to it. The elements that cause the callback function to return true are added to the new array. Take a look at the following example:

const words = [‘chair’, ‘music’, ‘pillow’, ‘brick’, ‘pen’, ‘door’];

const shortWords = words.filter(word => {
  return word.length < 6;
});

 

  • words is an array that contains string elements.
  • const shortWords = declares a new variable that will store the returned array from invoking .filter().
  • The callback function is an arrow function has a single parameter, word. Each element in the words array will be passed to this function as an argument.
  • word.length < 6; is the condition in the callback function. Any word from the words array that has fewer than 6characters will be added to the shortWords array.

Let’s also check the values of words and shortWords:

console.log(words); // Output: [‘chair’, ‘music’, ‘pillow’, ‘brick’, ‘pen’, ‘door’];
console.log(shortWords); // Output: [‘chair’, ‘music’, ‘brick’, ‘pen’, ‘door’]

 

Observe how words was not mutated, i.e. changed, and shortWords is a new array.

The .findIndex() Method

We sometimes want to find the location of an element in an array. That’s where the .findIndex() method comes in! Calling .findIndex() on an array will return the index of the first element that evaluates to true in the callback function.

const jumbledNums = [123, 25, 78, 5, 9];

const lessThanTen = jumbledNums.findIndex(num => {
  return num < 10;
});

 

  • jumbledNums is an array that contains elements that are numbers.
  • const lessThanTen = declares a new variable that stores the returned index number from invoking .findIndex().
  • The callback function is an arrow function has a single parameter, num. Each element in the jumbledNums array will be passed to this function as an argument.
  • num < 10; is the condition that elements are checked against. .findIndex() will return the index of the first element which evaluates to true for that condition.

Let’s take a look at what lessThanTenevaluates to:

console.log(lessThanTen); // Output: 3

 

If we check what element has index of 3:

console.log(jumbledNums[3]); // Output: 5

Great, the element in index 3 is the number 5. This makes sense since 5 is the first element that is less than 10.

If there isn’t a single element in the array that satisfies the condition in the callback, then .findIndex() will return -1.

const greaterThan1000 = jumbledNums.findIndex(num => {
  return num > 1000;
});

console.log(greaterThan1000); // Output: -1

The .reduce() Method

Another widely used iteration method is .reduce(). The .reduce() method returns a single value after iterating through the elements of an array, thereby reducing the array. Take a look at the example below:

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

const summedNums = numbers.reduce((accumulator, currentValue) => {
  return accumulator + currentValue
})

console.log(summedNums) // Output: 17

 

Here are the values of accumulator and currentValue as we iterate through the numbers array:

IterationaccumulatorcurrentValuereturn value
First123
Second347
Third71017

 

Now let’s go over the use of .reduce() from the example above:

  • numbers is an array that contains numbers.
  • summedNums is a variable that stores the returned value of invoking .reduce() on numbers.
  • numbers.reduce() calls the .reduce()method on the numbers array and takes in a callback function as argument.
  • The callback function has two parameters, accumulator and currentValue. The value of accumulator starts off as the value of the first element in the array and the currentValue starts as the second element. To see the value of accumulatorand currentValue change, review the chart above.
  • As .reduce() iterates through the array, the return value of the callback function becomes the accumulator value for the next iteration, currentValue takes on the value of the current element in the looping process.

The .reduce() method can also take an optional second parameter to set an initial value for accumulator (remember, the first argument is the callback function!). For instance:

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

const summedNums = numbers.reduce((accumulator, currentValue) => {
  return accumulator + currentValue
}, 100// <- Second argument for .reduce()

console.log(summedNums); // Output: 117

 

Here’s an updated chart that accounts for the second argument of 100:

Iteration #accumulatorcurrentValuereturn value
First1001101
Second1012103
Third1034107
Fourth10710117

Review

Let’s review what we learned in this lesson:

  • Abstraction allows us to write complicated code in a way that’s easy to reuse, debug, and understand for human readers
  • We can work with functions the same way we would any other type of data including reassigning them to new variables
  • JavaScript functions are first-class objects, so they have properties and methods like any object
  • Functions can be passed into other functions as parameters
  • A higher-order function is a function that either accepts functions as parameters, returns a function, or both
  • .forEach() is used to execute the same code on every element in an array but does not change the array and returns undefined.
  • .map() executes the same code on every element in an array and returns a new array with the updated elements.
  • .filter() checks every element in an array to see if it meets certain criteria and returns a new array with the elements that return truthy for the criteria.
  • .findIndex() returns the index of the first element of an array which satisfies a condition in the callback function. It returns -1 if none of the elements in the array satisfies the condition.
  • .reduce() iterates through an array and takes the values of the elements and returns a single value.
  • All iterator methods takes a callback function that can be pre-defined, or a function expression, or an arrow function.