Callback Functions in JavaScript

Functions are first class citizens in JavaScript.

This means JS functions can be passed to another function as an argument.

The function that we pass into another function as argument is a callback function.

Callback functions help us to perform asynchronous tasks in JS which is a synchronous single threaded language.

Let's look an an example.

setTimeout(function() {

console.log("set timeout called");

}, 3000);

function a(b) {

console.log("a called");

b();

}

a(function b() {

console.log("b called");

});

Output

a called

b called

set timeout called --after 3 secs

Behind the scenes:

Global execution context is created and it is inserted into the call stack.

JavaScript encounters set timeout, acknowledges its presence and stores it for execution after 3000ms.

function a is invoked, new execution context is created for function a, the ec is inserted to the call stack, and the callback function b is passed to it.

function a is executed, thus invoking function b.

function a is able to invoke function b since it has been passed to it.

function b's execution context is created, its ec is also inserted to the call stack, followed by its execution.

functions b's execution context is removed from the call stack followed by the removal of function a's execution context.

JavaScript is very fast, till now 3 secs have not passed, so JS is idle as nothing else is left to execute.

An important point to be noted here is that now the Global Execution Context will be removed from the call stack.

After 3 secs, the global execution context is created and inserted back to the call stack, followed by creation of the execution context of the callback function of setTimeout, which is also, you guessed it right, inserted to the call stack.

At this point the call stack has two execution contexts: global and one for the setTimeout.

The callback function is executed and its execution context is removed from the call stack.

Finally, the GEC is removed from the call stack.

JavaScript has only one call stack

If a function takes a huge amount of time to execute, the function should be converted to a callback function. Otherwise JS wouldn't be able to execute anything else while the complicated function is being executed and this might affect your website's performance.

We should never block our main thread i.e., our call stack.

We should always use asynchronous JS to execute things that might take more time.

Now imagine if functions were not first class citizens in JS.

We wouldn't be able to pass functions as arguments to other functions.

This means that callback functions would cease to exist.

This would mean that there's no asynchronous JS and everything would be executed synchronously.

JS wouldn't be the world's most popular language then.

Let's look at an important interview question now.

Write a JS code to detect how many times a button has been clicked on a webpage.

function closure() {

let count = 0;

const button = document.querySelector("button");

button.addEventListener("click", () => {

console.log(++count);

});

}

closure();

Here I'm keeping the event listener inside a closure so that the variable count is not accessible to anything outside and hence its value cannot be changed by anything outside the function.

The callback function listening to click events on the button forms a closure with its outer scope. We use closures for data hiding.

The current value of variable count is maintained by JS in the closure and hence it's updating the value correctly.

Need of removing event listeners

Event listeners are heavy i.e., it takes memory.

It forms a closure and JS keeps the variables/ functions declared in the closure in memory until you close the website on the browser as it's listening to event listners. It would have to stay alive, otherwise there's no point of having event listeners.

Once we remove event listeners from an element, all the memory held in closure/ scope will be garbage collected.

Example of removing event listener. Note that in order to remove anonymous callbacks, we need to store their reference, like in a variable.

#Adding event listener

const element = document.querySelector("button");

let handleClick = () => {

console.log("clicked");

}

element.addEventListener("click", handleClick)

#Removing event listener

element.removeEventListener("click", handleClick);

Ending with a fun fact: callback functions are on the mercy of the functions they're passed into. They cannot be called directly by the user. Hence the name.