Mastering Hard Parts of JavaScript: Callbacks I
Ryan Ameri
Posted on August 8, 2020
I'm currently undertaking JavaScript: The Hard Parts v2 course at Frontend Masters. It is a brilliant course taught by the amazing Will Sentance. The course goes over the following key concepts:
- Callbacks & Higher order functions
- Closure (scope and execution context)
- Asynchronous JavaScript & the event loop
- Classes & Prototypes (OOP)
In this tutorial series, I will go over the exercises given in each section, provide my own solution and provide a commentary as to how I came to that solution. This first part deals with Callbacks.
Callbacks are an inherently fundamental concept in JS, as most everything from closure to asynchronous JavaScript is built upon them. Prior to my introduction to JS, I had never encountered higher ordered functions (a function that can take another function as input, or return a function) so I initially found the concept very confusing. Thankfully, with lots of practice, I was able to get a good handle on callbacks. I'd encourage you to implement your own solutions first before looking at mine and then compare and contrast. There are certainly many different ways of solving these exercises and mine are definitely not necessarily the best. My solutions are all available on github and you are very welcome to fork the repo to work on your own or, if you have found a better way of solving these, send a PR.
If you are new to JS or have a hard time getting your head wrapped around callbacks, I think going through these exercises will help you master the concept. For more information, Will's slides for the course can be found here(pdf).
Exercise 1
Create a function addTwo that accepts one input and adds 2 to it.
console.log(addTwo(3))
should output 5
and
console.log(addTwo(10))
should output 12
Solution 1
function addTwo(num) {
return num + 2;
}
The most simple exercise. It gives us a nice comforting feeling knowing that we know how to use functions. Don't worry, things will get interesting soon!
Exercise 2
Create a function addS that accepts one input and adds an "s" to it.
console.log(addS("pizza"));
should output pizzas
and console.log(addS("bagel"));
should output bagels
Solution 2
function addS(word) {
return word + "s";
}
Another easy function. Good reminder that +
is an overloaded operator in JS that can work with strings and numbers.
Exercise 3
Create a function called map that takes two inputs:
an array of numbers (a list of numbers)
a 'callback' function - a function that is applied to each element of the array (inside of the function 'map')
Have map return a new array filled with numbers that are the result of using the 'callback' function on each element of the input array.
console.log(map([1, 2, 3], addTwo));
should output [ 3, 4, 5 ]
Solution 3
function map(array, callback) {
const newArr = [];
for (let i = 0; i < array.length; i++) {
newArr.push(callback(array[i]));
}
return newArr;
}
Now this is more interesting! We are basically re-implementing a simple version of the native Array.prototype.map() function here. I decided to use a basic for loop here as most people should be familiar with it. I think this is probably the most important exercise in the series, if you can get head around this, you've basically gotten callbacks!
Exercise 4
The function forEach takes an array and a callback, and runs the callback on each element of the array. forEach does not return anything.
let alphabet = "";
const letters = ["a", "b", "c", "d"];
forEach(letters, function (char) {
alphabet += char;
});
console.log(alphabet);
should output abcd
Solution 4
function forEach(array, callback) {
for (let i = 0; i < array.length; i++) {
callback(array[i]);
}
}
Another reimplementation of a native Array method. Notice the difference with map, map returns an array, forEach doesn't return anything so whatever needs to happen needs to take place in the body of the callback function.
Exercise 5
Rebuild your map function, this time instead of using a for loop, use your own forEach function that you just defined. Call this new function mapWith.
console.log(mapWith([1, 2, 3], addTwo));
should output [ 3, 4, 5 ]
Solution 5
function mapWith(array, callback) {
const newArr = [];
forEach(array, (item) => {
newArr.push(callback(item));
});
return newArr;
}
Using your own previously defined function in this manner is very powerful. It allows you to get to grips with how functions exactly work. Now when you use a library such as lodash or underscore, you can imagine how the underlying function is implemented.
Exercise 6
The function reduce takes an array and reduces the elements to a single value. For example it can sum all the numbers, multiply them, or any operation that you can put into a function.
const nums = [4, 1, 3];
const add = function (a, b) {
return a + b;
};
console.log(reduce(nums, add, 0))
should output 8
.
Solution 6
function reduce(array, callback, initialValue) {
let accum;
if (Object.keys(arguments).length > 2) {
accum = initialValue;
} else {
// InitialValue not provided
accum = array[0];
array.shift();
}
forEach(array, (item) => {
accum = callback(accum, item);
});
return accum;
}
Ah reduce! One of the most misunderstood yet powerful functions in JS (and more broadly in functional programming). The basic concept is this: You have an initial value, you run the callback function on every item in an array, and assign the result to this initial value. At the end, you return this value.
The other gotcha with reduce is that the initialValue parameter is optional, the caller might provide it or not. If it is provided, we should use its value as the initial accumulator of our array. If it's not provided, we should consider the first element of the array as the accumulator. Here we test the number of arguments provided by checking Object.keys(arguments).length
and proceed to set our accumulator accordingly.
Notice how we used our own forEach function, we could have of course also used the native array.forEach(), with the same behaviour.
Edit: Thanks to Jason Matthews (in the comments below) for pointing out that my previous solution (assigning initialValue
to itself) could have unintended side effects. By assigning to a new variable, we have made the function pure.
Edit 2: Thanks for Dmitry Semigradsky for picking up a bug in the reduce implementation!
Exercise 7
Construct a function intersection that compares input arrays and returns a new array with elements found in all of the inputs. BONUS: Use reduce!
console.log(
intersection([5, 10, 15, 20], [15, 88, 1, 5, 7], [1, 10, 15, 5, 20])
);
Should output [5, 15]
Solution 7
function intersection(...arrays) {
return arrays.reduce((acc, array) => {
return array.filter((item) => acc.includes(item));
});
}
Combining reduce and filter results in a powerful function. Here, if acc
is not provided as a param, it is set to the first array, and we are not providing it as an argument. So in subsequent calls we just filter the arrays to return items that were also included in the acc
` array.
Notice the use of ...arrays
, here we are using the rest parameters because we don't know how many arguments will be supplied to the function.
Posted on August 8, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.