Skip to main content

JavaScript Callback Functions

Learning Objectives

Students Will Be Able To:
Identify When a Function is a Callback Function
Use Anonymous & Named Functions as Callbacks
Use Callbacks with Array Iterator Methods
Use the filter Array Iterator Method to "filter" an Array
Use Callbacks with Asynchronous Operations

Road Map

  1. Setup
  2. What's a Callback Function?
  3. Why are Callbacks Needed?
  4. Using Callbacks with Array Iterator Methods
  5. Practice Exercise - Using the filter Iterator Method
  6. Using Callbacks with Asynchronous Functions
  7. Essential Questions
  8. Further Study

Videos

  1. Video 📹 Link
  2. Video 📹 Link
  3. Video 📹 Link

1. Setup

Create a new HTML/CSS/JS Repl in replit.com and name it Callback Functions.

2. What's a Callback Function?

A callback function is a function that's passed to another function as an argument.

function higherOrderFunction(callbackFunction) {
// Sooner or later...
callbackFunction();
}

A bit more vocabulary... A function that accepts a function as input and/or returns a function is known as a higher-order function.

note

Callback functions are not a new type of function, they are defined just like any other function (as a declaration, expression or arrow function).

Typically, the higher-order function accepts a callback for the purpose of invoking it - at least once, at some point in time.

Since you've previously used the array forEach method, you've actually already passed a callback function...

colors.forEach(function(color, idx) {
...
});

Although using anonymous functions as callbacks is convenient, it's not uncommon to use a named function for code organization:

document.getElementById('todo-container')
.addEventListener('click', handleTodoClick);

Or when the callback might be called from more than just one point in the code:

/*-- Event Listeners --*/
document.getElementById('replay-btn').addEventListener('click', init);

/*-- Functions --*/
init();

function init() {
// Initialize state and call render
}
danger

👀 Be careful not to invoke the callback when passing it, i.e., do not put parentheses after the function! Otherwise, you'll be passing the result returned by that function instead of the function itself.

Don't Do This:

document.getElementById('todo-container')
.addEventListener('click', handleTodoClick());
// No parens please ^^

❓ Review Questions - Callback Functions (1 min)

(1) What's a callback function?


A function that's passed as an argument to another function
(usually to be called by that function at least once, at some point)


const colors = ['red', 'green', 'blue'];

colors.forEach(function(color, idx) {
console.log(`${idx + 1} - ${color}`);
});

Refer to the above code when answering the next two questions...

(2A) What part of the code is the callback function?


👉function(color, idx) {
console.log(`${idx + 1} - ${color}`);
}👈

(2B) How many times will the higher-order function, forEach, invoke the callback?


3
(once for each element in the array)


3. Why are Callbacks Needed?

There are two scenarios that require the use of callbacks:

  1. Certain functions & methods - by design - need a block of code in order to perform their purpose. For example, both forEach & addEventListener need a block of code to run. Callback functions provide that code.

  2. Certain higher-order functions/methods execute asynchronously in the background and are designed to invoke callback functions when they have finished their task. More on this later...

4. Using Callbacks with Array Iterator Methods

One of the most popular use-cases for callback functions is to provide them to iterator methods on arrays.

The forEach method we've been using is a perfect example and has been designed to:

  1. Accept a callback function as an argument
  2. Invoke that callback once for each element in the array

5. 💪 Practice Exercise - Using the filter Iterator Method (20 min)

  1. Create a new HTML/CSS/JS Repl in replit.com and name it Filter Method Practice.

  2. Copy and paste the following cars array into the repl:

    const cars = [
    { make: 'Toyota', yrsOld: 5, mileage: 92399 },
    { make: 'Ford', yrsOld: 12, mileage: 255005 },
    { make: 'Ferrari', yrsOld: 9, mileage: 12966 },
    { make: 'Subaru', yrsOld: 9, mileage: 111266 },
    { make: 'Toyota', yrsOld: 2, mileage: 41888 },
    { make: 'Tesla', yrsOld: 3, mileage: 57720 }
    ];
  3. Research the array filter method.

info

👀 In summary, the filter method iterates over all of the elements in an array and returns a new array containing zero or more of the elements from that original array. The callback function determines which elements are included in the new array - if it returns a truthy value, the element for that current iteration will be included.

  1. Use the filter method to "select" the objects within the cars array that have been driven more than 20,000 miles per year:
tip

Hint: Like forEach, the filter method will invoke the callback once for each element in the cars array. Note that: Each element is an object, so each time the callback is invoked, its parameter will hold an object with make, yrsOld & mileage properties.

  1. Store the new array returned by cars.filter(...) in a variable named wellDrivenCars.

  2. You may use either an anonymous or a named function as the callback function provided to the filter method.

  3. Finally, use the forEach method on the wellDrivenCars array to console.log each "car" object.

We'll review a solution in 20 minutes...

5. Using Callbacks with Asynchronous Functions

JavaScript's asynchronous programming model is one of its more challenging concepts to learn, so don't despair if it takes a bit of time to understand.

Think of asynchronous programming as a way to allow multiple things to happen in a program at the same time...

Certain functions/methods are designed to be asynchronous because they take a long time to complete their task.

When an asynchronous function (or method) is called: it goes into the background to do its thing, allowing the remaining "synchronous" code to run to completion.

When the asynchronous operation has completed, it will call its callback function (or resolve its promise - yes....that's for another lesson 😉).

Consider the following code:

let sales;
console.log(sales); //-> undefined

// Assume getSalesData is an asynchronous function.
// When run, it will wait in the "background"...
// and call its callback function when the data arrives
getSalesData(function(salesFromDB) {
// Update sales variable when data arrives
sales = salesFromDB;
});

console.log(sales); //-> ???

❓ What will be logged out in the second console.log(sales) above?


undefined because getSalesData() will wait in the background until the data from the database is returned, meanwhile the remaining synchronous code runs.


For a better understanding of asynchronous code, we'll start by confirming what synchronous code is...

Synchronous Code Execution

So far, we've taken for granted that the code we write runs line-by-line, so if we call a function/method it finishes before the next line of code runs (i.e., our code executes synchronously).

For example:

const colors = ['red', 'green', 'blue'];

console.log('Code BEFORE the forEach...');

colors.forEach(function(color, idx) {
console.log(`${idx + 1} - ${color}`);
});

console.log('Code AFTER the forEach...');

Running the above results in this expected output:

Code BEFORE the forEach...
1 - red
2 - green
3 - blue
Code AFTER the forEach...

Now let's see what asynchronous operations look like...

Asynchronous Code Execution

Until we start working with databases, etc., we can use the asynchronous setTimeout() & setInterval() functions which, when called, wait in the background until a specified amount of time elapses, then they call their callback functions.

Let's see setTimeout() in action:

const colors = ['red', 'green', 'blue'];

console.log('Code BEFORE the forEach...');

// setTimeout accepts a callback & how long to wait before calling the cb
setTimeout(function() {
colors.forEach(function(color, idx) {
console.log(`${idx + 1} - ${color}`);
});
}, 1000); // 1000 milliseconds (1 sec)

console.log('Code AFTER the forEach...');

Running the above results in different output than the synchronous version:

Code BEFORE the forEach...
Code AFTER the forEach...
1 - red
2 - green
3 - blue

A Fun Asynchronous Example

The code below demonstrates how setTimeout and callback functions can be used to implement the scenario where we need to:

  • Do something...
  • Wait for some period of time...
  • Do something else...
  • Wait for some period of time...
  • etc.

🚦 Let's say we wanted a traffic light to cycle between Red, Green & Yellow lights with each light turned on for a certain amount of time:

  • Red light on for 3 seconds
  • Green light on for 2 seconds
  • Yellow light on for 1 second

First, let's copy/paste the HTML into index.html:

index.html
<body>
<!-- add the following HTML -->
<main>
<div></div> <!-- red -->
<div></div> <!-- yellow -->
<div></div> <!-- green -->
</main>

Now for some CSS:

style.css
body {
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}

main {
display: grid;
grid-template-columns: 20vmin;
grid-template-rows: repeat(3, 20vmin);
border: 2px solid grey;
}

main div {
margin: 2vmin;
border-radius: 50%;
border: 2px solid grey;
}
caution

REMINDER: Expect the following asynchronous code to be difficult to understand. However, the following example will come in handy if you choose to do a game like Simon or Concentration , a game that requires timing/sequencing.

For the JavaScript, let's start by defining a flexible data structure that makes it easy to modify the functionality (color and time):

// Using a "lookup" data-structure
// minimizes code and increases flexibility
const lightSequence = [
{color: 'red', time: 3000},
{color: 'green', time: 2000},
{color: 'yellow', time: 1000}
];

Next, cache the light <div>s and define a variable to track the current light:

// Cache the divs for the lights
const lightEls = document.querySelectorAll('main > div');

// Variable to track the current light
let curLightIdx = 0; // Start with red object

Now, let's make the function responsible for displaying the current light AND for letting its caller know when the time for the light has expired:

function renderLight(cb) {
// First, turn off all lights
lightEls.forEach(el => el.style.backgroundColor = 'black');
// Next, turn on the current light
lightEls[curLightIdx].style.backgroundColor = lightSequence[curLightIdx].color;
// Invoke the callback when this light's time has expired
setTimeout(cb, lightSequence[curLightIdx].time);
}

Finally, another function that increments the curLightIdx and calls renderLight while passing itself as a callback:

function renderLightSequence() {
renderLight(renderLightSequence);
// Increment and reset to zero when 3 is reached
curLightIdx = ++curLightIdx % 3;
}

// Make it start!
renderLightSequence();

Very cool!

Let's wrap up with some questions!

❓ 6. Essential Questions (2 mins)

(1) If asked in a job interview, "What's a callback function?" - what would a good answer be?


A function that's passed as an argument to another function.


(2) Are callback functions are defined differently than non-callback functions?


No


(3) Is the following code likely to work as expected?

document.getElementById('items')
.addEventListener('click', handleClick());

No, because the callback function, handleClick, is accidentally being invoked.


(4) Is the addEventListener method a higher-order function?


Yes, because it accepts a function as an argument. As a reminder, a function is also considered to be a higher-order function if it returns a function.


7. Further Study

The Browser's Event Loop

Here's a great video on how asynchronous operations - such as the handling of events - do their business and notify JavaScript when their work is done.

The Event Loop (in this video, the amazing and funny Jake Archibald from Google does an amazing job demonstrating the browser's event loop).

Event Loop & Queue Animation

There are browser "Web APIs" that keep track of events, setTimeouts, etc.

For example, when we call setTimeout() the browser keeps track of the timing. When the time expires, it will place the setTimeout's callback function into the callback/event queue to be executed as soon as there are no other functions running in the call stack.