Ah, callback functions. So quintessentially JavaScript!

What exactly is a callback function? It's just a function that you pass into another function as an argument. That's literally all it means.

As the MDN Web Docs put it:

A callback function is a function passed into another function as an argument, which is then invoked inside the outer function to complete some kind of routine or action.

A couple of years ago, I wrote about named callback functions (as opposed to anonymous callback functions). I mentioned that some native functions, such as the .forEach() array method, automatically pass arguments into the callback function that you provide.

For example, the callback function you pass into the .forEach() method automatically receives the element, index, and array as its arguments. There are a few ways we can prove this.

The first way is by passing the console.log() method to the .forEach() method. The console.log() method accepts an indefinite number of arguments; by using it as the callback function for the .forEach() method, it will print out all the arguments that are passed to it on each iteration:

const coffees = [
{ name: "Caffè Americano", price: 2.75 },
{ name: "Caffè Latte", price: 3.05 },
{ name: "Flat White", price: 3.05 },
];

coffees.forEach(console.log);

// { name: "Caffè Americano", price: 2.75 } 0 [
// { name: "Caffè Americano", price: 2.75 },
// { name: "Caffè Latte", price: 3.05 },
// { name: "Flat White", price: 3.05 }
// ]
// { name: "Caffè Latte", price: 3.05 } 1 [
// { name: "Caffè Americano", price: 2.75 },
// { name: "Caffè Latte", price: 3.05 },
// { name: "Flat White", price: 3.05 }
// ]
// { name: "Flat White", price: 3.05 } 2 [
// { name: "Caffè Americano", price: 2.75 },
// { name: "Caffè Latte", price: 3.05 },
// { name: "Flat White", price: 3.05 }
// ]

The second way is by using rest parameters. By collecting an indefinite number of args as an array, then spreading that array back into single arguments, we get the same output:

coffees.forEach(function (...args) {
console.log(...args);
});

// { name: "Caffè Americano", price: 2.75 } 0 [
// { name: "Caffè Americano", price: 2.75 },
// { name: "Caffè Latte", price: 3.05 },
// { name: "Flat White", price: 3.05 }
// ]
// { name: "Caffè Latte", price: 3.05 } 1 [
// { name: "Caffè Americano", price: 2.75 },
// { name: "Caffè Latte", price: 3.05 },
// { name: "Flat White", price: 3.05 }
// ]
// { name: "Flat White", price: 3.05 } 2 [
// { name: "Caffè Americano", price: 2.75 },
// { name: "Caffè Latte", price: 3.05 },
// { name: "Flat White", price: 3.05 }
// ]

The third way is to use the arguments object, which is accessible from all non-arrow functions. As above, by spreading it into single arguments, we get the same output:

coffees.forEach(function () {
console.log(...arguments);
});

// { name: "Caffè Americano", price: 2.75 } 0 [
// { name: "Caffè Americano", price: 2.75 },
// { name: "Caffè Latte", price: 3.05 },
// { name: "Flat White", price: 3.05 }
// ]
// { name: "Caffè Latte", price: 3.05 } 1 [
// { name: "Caffè Americano", price: 2.75 },
// { name: "Caffè Latte", price: 3.05 },
// { name: "Flat White", price: 3.05 }
// ]
// { name: "Flat White", price: 3.05 } 2 [
// { name: "Caffè Americano", price: 2.75 },
// { name: "Caffè Latte", price: 3.05 },
// { name: "Flat White", price: 3.05 }
// ]

OK, so we've established that the .forEach() method always passes in the element, index, and array, regardless of which parameters we actually define in our callback function. But how does this work under the hood?

Array methods from scratch

There are a number of native array methods whose callback functions have this signature:

Let's write these functions from scratch so we can see how they work! All of them will accept the following arguments:

  1. The array to iterate over
  2. The user-provided callback function
    1. The current element
    2. The current index
    3. The array itself
  3. An optional value, thisArg, to use as this when executing the callback

It goes without saying, but please don't actually use the following functions in production. Just use the native array methods.

If you want to test these functions, I've made a demo on StackBlitz.

forEach()

Let's start with an empty function declaration:

function forEach(array, callback, thisArg) {}

If thisArg is provided, we use the .bind() method to set it as the value of this for the callback function:

function forEach(array, callback, thisArg) {
if (thisArg) {
callback = callback.bind(thisArg);
}
}

Then we loop through the array using a simple for loop, calling the callback function once for each element. Notice that when we invoke callback(), we pass in the current element (array[i]), the current index (i), and the array itself (array):

function forEach(array, callback, thisArg) {
if (thisArg) {
callback = callback.bind(thisArg);
}

for (let i = 0; i < array.length; i++) {
callback(array[i], i, array);
}
}

Here's a simple demo. On each iteration, it logs all arguments passed into the console.log() method:

const coffees = [
{ name: "Caffè Americano", price: 2.75 },
{ name: "Caffè Latte", price: 3.05 },
{ name: "Flat White", price: 3.05 },
];

forEach(coffees, console.log);

// { name: "Caffè Americano", price: 2.75 } 0 [
// { name: "Caffè Americano", price: 2.75 },
// { name: "Caffè Latte", price: 3.05 },
// { name: "Flat White", price: 3.05 }
// ]
// { name: "Caffè Latte", price: 3.05 } 1 [
// { name: "Caffè Americano", price: 2.75 },
// { name: "Caffè Latte", price: 3.05 },
// { name: "Flat White", price: 3.05 }
// ]
// { name: "Flat White", price: 3.05 } 2 [
// { name: "Caffè Americano", price: 2.75 },
// { name: "Caffè Latte", price: 3.05 },
// { name: "Flat White", price: 3.05 }
// ]

map()

Let's start with another empty function declaration:

function map(array, callback, thisArg) {}

The native .map() method returns a new array with the results of calling the callback function once for each element in the original array. As such, let's assign a newArray. We'll also bind the optional thisArg again:

function map(array, callback, thisArg) {
const newArray = [];

if (thisArg) {
callback = callback.bind(thisArg);
}
}

Now we just need to loop through the original array, calling the callback function once for each element, and pushing the return value into the newArray. Then we can return the newArray:

function map(array, callback, thisArg) {
const newArray = [];

if (thisArg) {
callback = callback.bind(thisArg);
}

for (let i = 0; i < array.length; i++) {
newArray.push(callback(array[i], i, array));
}

return newArray;
}

Here's a simple demo. It creates a new array of the coffees' prices only:

const coffees = [
{ name: "Caffè Americano", price: 2.75 },
{ name: "Caffè Latte", price: 3.05 },
{ name: "Flat White", price: 3.05 },
];

const prices = map(coffees, function (coffee) {
return coffee.price;
});

console.log(prices);

// [ 2.75, 3.05, 3.05 ]

filter()

Like the .map() method, the native .filter() method returns a new array. The new array contains the elements from the original array that returned a truthy value from the callback function. As with our map() function, let's start with the newArray assignment and thisArg binding:

function filter(array, callback, thisArg) {
const newArray = [];

if (thisArg) {
callback = callback.bind(thisArg);
}
}

When we loop through the original array, we use a guard clause to check if the callback function returns a falsy value for the current element. If so, we use the continue statement to skip over that element. Otherwise, we add it to the newArray. Once we're done looping through the original array, we return the newArray:

function filter(array, callback, thisArg) {
const newArray = [];

if (thisArg) {
callback = callback.bind(thisArg);
}

for (let i = 0; i < array.length; i++) {
if (!callback(array[i], i, array)) continue;
newArray.push(array[i]);
}

return newArray;
}

Here's a simple demo. It creates a new array by filtering out the coffees that cost less than £3:

const coffees = [
{ name: "Caffè Americano", price: 2.75 },
{ name: "Caffè Latte", price: 3.05 },
{ name: "Flat White", price: 3.05 },
];

const expensiveCoffees = filter(coffees, function (coffee) {
return coffee.price >= 3;
});

console.log(expensiveCoffees);

// [
// { name: "Caffè Latte", price: 3.05 },
// { name: "Flat White", price: 3.05 }
// ]

find()

For our find() function, let's start with the thisArg binding:

function find(array, callback, thisArg) {
if (thisArg) {
callback = callback.bind(thisArg);
}
}

When we iterate over the array, we check if the callback function returns a truthy value for the current element. If so, we return that element. If the callback function doesn't return a truthy value for any of the elements, then the for loop will finish executing, and the function will implicitly return undefined:

function find(array, callback, thisArg) {
if (thisArg) {
callback = callback.bind(thisArg);
}

for (let i = 0; i < array.length; i++) {
if (callback(array[i], i, array)) return array[i];
}
}

Here's a simple demo. It finds the first coffee that costs at least £3:

const coffees = [
{ name: "Caffè Americano", price: 2.75 },
{ name: "Caffè Latte", price: 3.05 },
{ name: "Flat White", price: 3.05 },
];

const caffeLatte = find(coffees, function (coffee) {
return coffee.price >= 3;
});

console.log(caffeLatte);

// { name: "Caffè Latte", price: 3.05 }

findIndex()

Our findIndex() function is nearly identical to our find() function. The difference is that, if we find a match, we return the current index instead of the current element. If there are no matches, we return -1:

function findIndex(array, callback, thisArg) {
if (thisArg) {
callback = callback.bind(thisArg);
}

for (let i = 0; i < array.length; i++) {
if (callback(array[i], i, array)) return i;
}

return -1;
}

Here's a simple demo. It finds the index of the first coffee that costs at least £3:

const coffees = [
{ name: "Caffè Americano", price: 2.75 },
{ name: "Caffè Latte", price: 3.05 },
{ name: "Flat White", price: 3.05 },
];

const caffeLatteIndex = findIndex(coffees, function (coffee) {
return coffee.price >= 3;
});

console.log(caffeLatteIndex);

// 1

every()

For our every() function, let's start with the thisArg binding again:

function every(array, callback, thisArg) {
if (thisArg) {
callback = callback.bind(thisArg);
}
}

The native .every() method returns true if every element in the array returns a truthy value from the callback function. Thus, when we loop through the array, if any of the elements returns a falsy value, we can return false. If we get to the end of the loop, and this hasn't happened, then all of the elements must have returned a truthy value, so we can return true:

function every(array, callback, thisArg) {
if (thisArg) {
callback = callback.bind(thisArg);
}

for (let i = 0; i < array.length; i++) {
if (!callback(array[i], i, array)) return false;
}

return true;
}

Here's a simple demo. It checks if every coffee costs less than £5:

const coffees = [
{ name: "Caffè Americano", price: 2.75 },
{ name: "Caffè Latte", price: 3.05 },
{ name: "Flat White", price: 3.05 },
];

function costsLessThanFive(coffee) {
return coffee.price < 5;
}

console.log(every(coffees, costsLessThanFive));

// true

some()

The .some() method is similar to the native .every() method. Instead of checking if every element returns a truthy value from the callback function, it checks if some of the elements—in other words, at least one of the elements—returns a truthy value.

When we loop through the array, we check if the current element returns a truthy value from the callback function. If so, that means that some (at least one) of the elements return a truthy value, so we return true. If we get to the end of the loop, and this hasn't happened, then it must mean that none of the elements returned a truthy value, so we return false:

function some(array, callback, thisArg) {
if (thisArg) {
callback = callback.bind(thisArg);
}

for (let i = 0; i < array.length; i++) {
if (callback(array[i], i, array)) return true;
}

return false;
}

Here's a simple demo. It checks if some of the coffees cost less than £3:

const coffees = [
{ name: "Caffè Americano", price: 2.75 },
{ name: "Caffè Latte", price: 3.05 },
{ name: "Flat White", price: 3.05 },
];

function costsLessThanThree(coffee) {
return coffee.price < 3;
}

console.log(some(coffees, costsLessThanThree));

// true

Next steps

All of the callback functions in this blog post are synchronous. But callback functions are often used to handle asynchronous operations. Next week, we'll use what we've learned today to help us understand asynchronous callback functions.