Higher Order Function, Callbacks, and Callback Hell in JavaScript.

In programming, functions are fundamental building blocks. But in JavaScript functions could not only perform their own task but also work with other functions. Higher-order functions and callbacks are powerful concepts for asynchronous programming in JavaScript, but they can lead to an unwanted situation known as callback hell.

Higher Order Functions

A higher-order function (HOF) is a function that does one of two things:

  1. Takes one or more functions as arguments: This allows you to pass functionality around as easily as you pass data. For example

    • forEach : calls the provided callback function for each element in an array in ascending-index order. always return undefined.

        const numbers = [1, 2, 3, 4, 5];
        numbers.forEach( (number) => { 
            console.log(number); // Output: 1 2 3 4 5
        });
      
    • map : calls a provided callback function once for each element in an array and constructs a new array from the results.

        const numbers = [1, 2, 3, 4];
        const squaredNumbers = numbers.map((number) => {
            return number * number
        });
        console.log(squaredNumbers); // Output: [1, 4, 9, 16]
      
    • filter : A shallow copy of the given array containing just the elements that pass the test. If no elements pass the test, an empty array is returned.

        const numbers = [1, 2, 3, 4, 5];
        const evenNumbers = numbers.filter( (number) => {
            return number % 2 === 0;
        });
        console.log(evenNumbers); // Output: [2, 4]
      
    • find : it calls a provided callback function once for each element in an array in ascending-index order until the callback returns a truthy value. find() then returns that element and stops iterating through the array. If callback never returns a truthy value, find() returns undefined.

    •   const numbers = [1, 2, 3];
        const firstEvenNumber = numbers.find((number) => {
          return number % 2 === 0;
        });
        console.log(firstEvenNumber); // Output: 2
      
    • reduce: It runs a "reducer" callback function over all elements in the array, in ascending-index order, and accumulates them into a single value. Every time, the return value of the callback function is passed into callbackFn again on the next invocation as the accumulator. The final value of the accumulator (which is the value returned from the callback function on the final iteration of the array) becomes the return value of reduce().

        const numbers = [1, 2, 3, 4, 5];
      
        const sumOfAllNumbers = numbers.reduce( 
            (accumulator, currentValue) => { 
                return (accumulator + currentValue);
        }, 0 ); // 0 is the initial value
        console.log(sumOfAllNumbers); // Output: 15
      
  2. Returns a new function: This lets you create functions on the fly, based on specific needs.

     function powerFunction(power) {
    
       return function(number) {
         return number ** power;
       }
     }
    
     const squareOf = powerFunction(2);
     const cubeOf = powerFunction(3);
    
     console.log(squareOf(12)); // Output: 144 (12*12)
     console.log(cubeOf(20)); // Output: 8000 (20*20*20)
    

CallBack Functions

A callback function is a function passed as an argument to another function. The receiving function (often a HOF) then invokes the callback at some point during its execution. This allows the callback to perform specific actions based on the results or events generated by the receiving function.

Callbacks are essential for asynchronous programming, where operations might take time to complete (like fetching data from a server). The callback provides a way to specify what code should run after the asynchronous operation finishes. Let's look at some Examples

function sum ( num1, num2 ) {
    console.log( num2 + num2 );
}
function subract (num1, num2 ) {
    console.log( num1 - num2 );
}
function calculate( num1, num2, callbackOperation ) {
    callbackOperation(num1, num2);
}

calculate(10, 5, sum);         // Output: 15 
calculate(95, 50, subract);    // Output: 45
const button = document.getElementById("myButton");
const clickCount = document.getElementById("clicks");
let count = 0;
// Callback function to update click count
function updateClickCount() {
    count++;
    clickCount.textContent = count + " clicks";
}
// Add event listener to button
button.addEventListener("click", updateClickCount);
function addToWaitingList(name, callback) {
  // Simulate adding the name to a waiting list (e.g., console log)
  console.log("Adding " + name + " to the waiting list...");

  // Simulate a delay (replace with actual asynchronous operation later)
  setTimeout(function() {
    callback(name);
  }, 2000); // Simulate 2 seconds wait
}

function notify(name) {
  console.log("Hello, " + name + "! Your table is ready.");
}

addToWaitingList("Alice", notify); // Add Alice to list and call notify after wait

console.log("Welcome! Please wait for your table.");

CallBack Hell

While HOFs and callbacks are powerful, nesting callbacks deeply can lead to code that's difficult to read and maintain. This situation is named "callback hell." Imagine a pyramid structure, where each layer represents a nested callback. The deeper you go, the harder it is to follow the flow of logic.

function getData(dataId, getNextData) {
  setTimeout(() => {
    console.log("data", dataId);
    if (getNextData) {
      getNextData();
    }
  }, 2000);
}

getData(1, () => {
  console.log("getting data2 .... ");
  getData(2, () => {
    console.log("getting data3 .... ");
    getData(3, () => {
      console.log("getting data4 .... ");
      getData(4, () => {
        console.log("getting data5 .... ");
        getdata(5);
        });
    });
  });
});
const cart = ["shoes", "pants", "kurta"];

api.createOrder(cart, function () {
    api.proceedToPayment(function () {
        api.showOrderSummary(function () {
            api.updateWallet()
        });
    });
});
fetchData(url, function(data) {
  processData(data, function(processedData) {
    displayData(processedData);
  });
});

Escaping Callback Hell

  • Promises: Promises provide a more structured way to handle asynchronous operations. They represent the eventual completion (or failure) of an asynchronous task.

  • Async/Await: Async/await syntax builds on top of Promises, offering a more synchronous-like way to write asynchronous code.