Callbacks & Higher Order Functions in JavaScript

oalbacha

Omar Albacha 🔅

Posted on December 29, 2021

Callbacks & Higher Order Functions in JavaScript
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;
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Before we go any further with this article, there are a couple of points to agree on regarding JavaScript:

  1. When thinking about code in JS, think in terms of values and not so much as labels or names of functions or variable.
    variables and values

  2. The Global Scope, AKA global memory AKA global execution context AKA Global thread, is the default scope when we run any JS code in the browser or in node.
    global memory
    **side note: the global scope is called (window) in the browser and (global) in node.

  3. Functions have their own scope called the function scope, AKA function local memory, function execution context.
    function local memory

  4. The call stack runs the global 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
    the call stack

  5. Functions in JS have 2 parts:

    • A label: the name of the function and
    • The code that is assigned to the label function parts

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);
Enter fullscreen mode Exit fullscreen mode

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.
copy Array And Manipulate function definition

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.
multiply by 2 function definition

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
The result variable declaration

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;
}
Enter fullscreen mode Exit fullscreen mode

Let’s try to simulate what happens in the local memory of the copyArrayAndManipulate function:
copy array and manipulate function's local memory

  1. We assign a local function input/variable called array to the value [1, 2, 3]
    copy array and manipulate function's local memory array input
    copy array and manipulate function's local memory array input code

  2. We assign a local function input/variable called instructions to the function definition (not label) of the function previously know as multiplyBy2.

copy array and manipulate function's local memory instruction function as input

copy array and manipulate function's local memory instruction function as input code

  1. We initialize and assign a local function variable called output to the value [] —empty array
    copy array and manipulate function's local memory output variable
    copy array and manipulate function's local memory output variable code

  2. Next we iterate through the array[1, 2, 3] using the for..loop. Note that instructions(array[i]) function is called in the this step inside the for..loop.
    copy array and manipulate function's local memory loop

This means 2 things:

  • We are going to are going to populate the array through output.push, calling the instructions() 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
}
Enter fullscreen mode Exit fullscreen mode
  • starts and begin with i = 0, array[0] = 1, and 1 * 2 = 2 the execution context of multipleBy2 ends and so it is removed/deleted from the call stack. 2 is returned and pushed into the output array output = [2]

First iteration: calls the instruction function with the input: 1, the function is now on top of the call stack:
copy array and manipulate function's local memory loop 1st iteration

Second iteration: calls the instruction function with the input: 2, the function is now again on top of the call stack:
copy array and manipulate function's local memory loop 2nd iteration

Third iteration: calls the instruction function with the input: 3, the function is now again on top of the call stack:
copy array and manipulate function's local memory loop 2nd iteration

  • loop ends with the value array [2, 4, 6] which will be assigned to the output array constant and we exit the copyArrayAndManipulate function local memory back out to global memory copy array and manipulate function's local memory loop end and return saved to output array

Finally, the value [2,4,6] is saved into the global variable value result
the output array is saved to the global variable 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:

  1. they can be assigned to variables and/or properties of other objects
  2. passed as arguments into other functions
  3. 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);
Enter fullscreen mode Exit fullscreen mode

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:

💖 💪 🙅 🚩
oalbacha
Omar Albacha 🔅

Posted on December 29, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related