10. Higher Order Functions & Callbacks

Follow along with code examples here!

Table of Contents:

Summary

  • Functions are "first-class" values meaning they can be stored in variables and data structures (arrays/objects), passed to functions as arguments, and returned from functions.

  • A method is a function stored in an object.

  • Higher-order functions (HOFs) are functions that accept other functions as input and/or return functions, enabling powerful programming patterns and code reuse.

  • Callback functions are functions passed as arguments to higher-order functions, allowing you to customize behavior without modifying the HOF itself.

  • When passing callbacks to HOFs, avoid invoking them (don't use parentheses) - the HOF will handle the invocation with the correct parameters.

  • Inline callback functions (anonymous arrow functions) provide a concise way to define callbacks when they won't be reused elsewhere.

  • Common HOF examples include setTimeout, setInterval, and array methods like forEach, which demonstrate how callbacks enable asynchronous and iterative programming.

  • Understanding HOFs and callbacks is fundamental to modern JavaScript development and functional programming concepts.

Functions are "First-Class" Values

Pop Quiz! Identify the data types stored in each variable below:

const a = 'hello world';
const b = 42;
const c = true;
const d = [1, 2, 3];
const e = { name: 'Ada' };
const f = () => { 
  console.log("hello world"); 
};
Answers
  1. String

  2. Number

  3. Boolean

  4. Array

  5. Object

  6. Function

What we can see from this is that functions are just another type of data and, like other dat types, it can be stored in a variable. This ability to be stored in a variable is the first quality that describes functions as "first class" values.

Functions Stored in Objects are Referred to as "Methods"

Functions can also be stored in objects and, less commonly, arrays. When stored in an object, we refer to the function as a method.

const methods = {
  sayHi: () => {
    console.log("Hi");
  },
  greet: (name) => {
    console.log(`Hello ${name}`);
  },
  isEven: (number) => {
    return number % 2 === 0;
  }
};

methods.sayHi();                // Hi
methods.greet('Ada');           // Hello Ada
console.log(methods.isEven(4)); // true

Try defining your own method called isOdd and test it out!

Functions Passed into Other Functions Are "Callbacks". The Function That Receives the Callback is a "Higher Order Function".

Here is a "normal" function that takes in a string as an argument:

const greet = (name) => {
  console.log(`Hello ${name}`);
}

greet('Ada');  // Passing a string as an argument

Just like strings, functions can be passed to other functions as arguments. In these cases, we call the function argument a "callback function" and the function receiving the callback is a "higher order function"

// First, we define the callback
const tick = () => {
  console.log("Tick");
}
setInterval(tick, 1000); // This higher order function takes in a callback and a number

// setTimeout also takes in a callback and a number. 
// Here, we define the callback "anonymously" or "inline"
setTimeout(() => {
  console.log("10 seconds have passed");
}, 10000);

Two classic examples of higher-order functions that are built into Node are:

  1. setTimeout(callback, delay)

  2. setInterval(callback, delay)

Each function takes in a callback function and a delay number as arguments and then invoke the callback after delay milliseconds have passed.

Q: Can you tell the difference between how setTimeout and setInterval work?

Answer

setTimeout only invokes the callback once whereas setInterval will continuously invoke the callback.

Some Fun Examples

This one creates a "loading wheel" animation:

2-fun-examples.js
const chars = ["\\", "|", "/", "-"];
let i = 0;

// empty the console each time and print the next character in the sequence
const loopThroughChars = () => {
  console.clear()
  console.log(chars[i]);
  i++;
  if (i >= 4) i = 0;
};

setInterval(loopThroughChars, 250);

This one animates an alien that bounces across the screen:

2-fun-examples.js
let str = '👾';
let forward = true;

const animateAlien = () => {
  // console.clear();   // uncomment this for a single-row experience
  console.log(str);

  if (forward) {
    // add white space to the front
    str = ' ' + str;
  } else {
    // remove 1 white space from the front
    str = str.slice(1);
  }

  // Turn around when reaching either side of the terminal
  const terminalWidth = process.stdout.columns;
  if (str.length === terminalWidth || str.length === 2) {
    forward = !forward;
  }
};

Do Not Invoke The Callback

When passing in a callback to a higher-order function, avoid invoking the callback.

In this example, since executeCallback is the higher-order function, it will invoke the callback on our behalf. Invoking the callback will produce an error:

executeCallback(sayHello);
// hello world

executeCallback(sayHello());
// TypeError: callback is not a function
Q: Why does an error get thrown? Why is it saying callback is not a function

When you invoke executeCallback(sayHello()), the sayHello() function call gets resolved first. Since sayHello() returns undefined, that is what executeCallback gets as its callback:

sayHello() // this gets resolved first and returns undefined
executeCallback(undefined) // undefined is not a function

Array Iterators

The most commonly used higher-order functions are these "iterator" array methods:

Method
Description (For every value in the source array...)
Example(s)

array.forEach

Perform a task that produces a side effect using the value

Mutate each value in the array or print each value to the console

array.filter

Test the value and add it to a new array if it passes the test

Keep only the even numbers in the source array

array.map

Transform the value and add it to a new array

Double every number in the source array

array.find

Test the value and return the first value that passes the test

Find the first number in an array that is greater than 10

array.findIndex

Test the value and return the index of the first value that passes the test

Find the index of the first number in an array that is greater than 10

array.reduce

Determine how it can be combined with the other values in the array

Calculate the sum of all numbers in an array

array.sort

Compare it with another value to determine which should go first

Sort an array of numbers in descending order

In the next lesson, we'll learn more about these but for now, let's focus on forEach

forEach

arr.forEach(callback) iterates over the source array and invokes the given callback with each value, that value's index, and the source array arr as inputs.

Use forEach to execute callbacks that produce side-effects (like printing to the console or mutating values).

In this example, we want to print a message about each value and its location in the fruits array:

2-forEach.js
const fruits = ['apples', 'bananas', 'cherries'];

// print every fruit and its position in the array!
fruits.forEach((input, i, arr) => {
  console.log(`${input}!!! at index ${i} in array [${arr}]`);
});

// Here we only use the value parameter
fruits.forEach((value) => {
  console.log(`${input}!!!`);
});

Remember, array.forEach is used for callbacks that result in a side effect. For example, mutating the contents of an array is considered a side effect. In this example, we use user.forEach to change the isAdmin property of every object in the users array:

2-forEach.js
// revoke isAdmin status from all users
const users = [
  { id: 1, username: 'ben', isAdmin: false },
  { id: 2, username: 'maya', isAdmin: true },
  { id: 3, username: 'reuben', isAdmin: true },
  { id: 4, username: 'gonzalo', isAdmin: false },
];
users.forEach((user) => user.isAdmin = false)

Challenge 1:

Use users.forEach to update the username property for each object in the users array such that the first letter is capitalized.

Solution
users.forEach((user) => user[0].toUppercase() + user.slice(1));

Challenge 2:

Implement your own forEach function that takes an array and a callback:

It should iterate through the input array and do the following:

  • Invoke the callback with the following arguments:

    • The value at the current index

    • The current index

    • The source array itself

  • Return nothing (or manually return undefined)

Usage Example

const forEach = (array, callback) => {
  // ??? 
};

forEach(['a','b','c'], console.log);
/* 
a 0 [ 'a', 'b', 'c' ]
b 1 [ 'a', 'b', 'c' ]
c 2 [ 'a', 'b', 'c' ]
*/
Solution
const forEach = (array, callback) => {
  // Iterate through the input array
  for (let i = 0; i < array.length; i++) {
    callback(array[i], i, array);
  }
}

Last updated