Callbacks & Higher Order Functions in JavaScript
Omar Albacha 🔅
Posted on December 29, 2021
Photo by Priscilla Du Preez on Unsplash
Javascript’s functional programming just like any other functional programming language like Common Lisp, Clojure, Elixir, OCaml, and Haskell is the idea that programs can construct programs by composing functions and applying them. Declaratively, we can write function definitions as trees of expressions that map values to other values, rather than a sequence of imperative statements which update the running state of the program.
JS treats functions as first-class citizens, meaning that they can be bound to names (including local identifiers), passed as arguments, and returned from other functions, just as any other data type can. This allows programs to be written in a declarative and composable style, where small functions are combined in a modular manner. One of the main concepts of functional programming which is the topic in discussion of this post is Callbacks & higher order functions.
To begin, we are going to examine a simple, regular and profoundly useless function that squares the number 3
.
function () {
return 3 * 3;
}
This is stupid and defies the fundamentals of functions and why we use them. There is no variability or reuse that we can get out of it. We can't use it to square another number or get a different value out of it. Not good!
So what we can do to make the function more reusable is generalize it taking the hard-coded number out and leaving that up to whoever calls the function to assign the number as input to the function they want to square.
function squareNum (num) {
return num * num
}
Now, we are deciding what data to apply our multiplication functionality to; only when we run our function, not when we define it. Let’s also see why we may not want to decide exactly what our functionality is until we run our function. Making our functionality reusable is the essence of why we have our higher order functions
Let's stretch the study of callbacks and higher-order functions idea by examining these functions:
function copyArrayAndMultiplyBy2 (array) {
const output = [];
for (let i = 0; i < array.length; i++) {
output.push(array[i] * 2);
}
return output;
}
function copyArrayAndDivideBy2 (array) {
const output = [];
for (let i = 0; i < array.length; i++) {
output.push(array[i] / 2);
}
return output;
}
function copyArrayAndAdd3 (array) {
const output = [];
for (let i = 0; i < array.length; i++) {
output.push(array[i] + 3);
}
return output;
}
const myArray = [1, 2, 3];
copyArrayAndMultiplyBy2 (myArray); // [2, 4, 6]
copyArrayAndDivideBy2 (myArray); // [0.5., 1, 1.5]
copyArrayAndAdd3 (myArray); // [4, 5, 6]
The functions above are very similar, the only difference is how the loop works either multiplying, dividing or adding to each element in the array. This code isn’t DRY, we are repeating ourselves.
What we can do to get better functionality and re-usability out of the previous code is by generalizing the function like we did before. We can have a generic MAMA function called copyArrayAndManipulate
that takes not only array
as an input but also a BABY function we are going to call (instructions
). This way, we can break out all the little parts that are different in the functions above and have ONE mama function for all the redundant code. DRY!
function multiplyBy2 (input) {
return input * 2
}
function divideBy2 (input) {
return input / 2
}
function add3 (input) {
return input + 3);
}
function copyArrayAndManipulate (array, instructions) {
const output = [];
for (let i = 0; i < array.length; i++) {
output.push(instructions(array[i]));
}
return output;
}
let result = copyArrayAndManipulate([1,2,3], multiplyBy2);
let result = copyArrayAndManipulate([1,2,3], divideBy2);
let result = copyArrayAndManipulate([1,2,3], add3);
Before we go any further with this article, there are a couple of points to agree on regarding JavaScript:
When thinking about code in JS, think in terms of values and not so much as labels or names of functions or variable.
The
Global Scope
, AKAglobal memory
AKAglobal execution context
AKAGlobal thread
, is the default scope when we run any JS code in the browser or in node.
**side note: the global scope is called (window) in the browser and (global) in node.Functions have their own scope called the
function scope
, AKAfunction local memory
,function execution context
.
The
call stack
runs theglobal memory
and stacks invoked functions on top of the stack in the order they were called in our code. Like a stack of plates, we are only concerned with the top of the stack
-
Functions in JS have 2 parts:
- A label: the name of the function and
- The code that is assigned to the label
Now that we got that out of the way, let’s walk through the code above line-by-line to better understand what happens when we call the mama copyArrayAndManipulate
with one of the baby functions, say multiplyBy2
:
function copyArrayAndManipulate (array, instructions) {
const output = [];
for (let i = 0; i < array.length; i++) {
output.push(instructions(array[i]));
}
return output;
}
function multiplyBy2 (input) { return input * 2 }
let result = copyArrayAndManipulate([1,2,3], multiplyBy2);
First, we declare a function definition into the global execution context (global memory) called copyArrayAndManipulate
. Note that we don’t run the code inside the function yet until it’s called.
Then, we declare a function definition into the global execution context (global memory) called multiplyBy2
. Note that we don’t run the code inside the function yet until it’s called.
In the last line, we declare a variable definition into the global execution context called result
which is undefined
for now until the function copyArrayAndManipulate
is run and returns a value
to be assigned to result
Since we called the copyArrayAndManipulate
when defining the result
variable, we need to run it and the returned value to be stores in the result
variable and so we start by running the code inside of it and go into its local function memory
function copyArrayAndManipulate (array, instructions) {
const output = [];
for (let i = 0; i < array.length; i++) {
output.push(instructions(array[i]));
}
return output;
}
Let’s try to simulate what happens in the local memory of the copyArrayAndManipulate
function:
We assign a local function input/variable called
array
to thevalue [1, 2, 3]
We assign a local function input/variable called
instructions
to the function definition (not label) of the function previously know asmultiplyBy2
.
We initialize and assign a local function variable called
output
to thevalue []
—empty array
Next we iterate through the
array[1, 2, 3]
using thefor..loop
. Note thatinstructions(array[i])
function is called in the this step inside thefor..loop
.
This means 2 things:
We are going to are going to populate the array through
output.push
, calling theinstructions()
function 3 times, once for each element in the array.The call stack is going to have the
instructions()
function run 3 times once per array element. It will run, populate the array with the returned value and get deleted (garbage collected).
We we call the instructions
function which is now equal to the multipleBy2
definition (not the label) meaning it has its same code, we enter its own execution context:
function multiplyBy2 (input) {
return input * 2
}
- starts and begin with
i = 0
,array[0] = 1
, and1 * 2 = 2
the execution context ofmultipleBy2
ends and so it is removed/deleted from the call stack.2
is returned and pushed into theoutput
arrayoutput = [2]
First iteration: calls the instruction function with the input: 1
, the function is now on top of the call stack:
Second iteration: calls the instruction function with the input: 2
, the function is now again on top of the call stack:
Third iteration: calls the instruction function with the input: 3
, the function is now again on top of the call stack:
- loop ends with the value
array [2, 4, 6]
which will be assigned to theoutput
array constant and we exit the copyArrayAndManipulate function local memory back out to global memory
Finally, the value [2,4,6]
is saved into the global variable value result
How was this possible?
Functions in javascript are first-class objects. They can co-exist and can be treated like any other JS object:
- they can be assigned to variables and/or properties of other objects
- passed as arguments into other functions
- returned as value from other functions (closure)
Functions have one property that JS objects do not have, they can be invoked/called/run.
In the example above: copyArrayAndManipulate
is our higher order function. takes in a function and passes out a function call multiplyBy2
which is our callback function
copyArrayAndManipulate([1,2,3], multiplyBy2);
callbacks & higher order functions keep our code simple and DRY. lots of the underlying javascript powerful that allows us to run complex things like asynchronous code rely on this concept.
sources:
Posted on December 29, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.