Home > Posts > Mastering JavaScript Functions: Understanding Execution Contexts, First-Class Functions, Higher-Order Functions, and Closures

Mastering JavaScript Functions: Understanding Execution Contexts, First-Class Functions, Higher-Order Functions, and Closures

Mohit Ranjan
Mohit Ranjan
Cover Image for Mastering JavaScript Functions: Understanding Execution Contexts, First-Class Functions, Higher-Order Functions, and Closures
Mohit Ranjan
Mohit Ranjan

How the functions work in Javascript

Pre Requisites

Execution Context :

Think of it as an environment where a code block runs, a box with variables, functions and more. It is a context (information) for lines of code to be executed. Execution context can be local and global. There can be multiple local contexts in a global context, just like boxes inside a bigger box. There can also be more execution contexts inside a local context and it could go on like a tree. Example : This code snippet

const x = 99
function one(){
    let y = 88
    function two(){
        let z = 77
    }
}

would look like this as execution context context-boxes

How does this work?

Each execution context have their own memory block, so that they could store all the functions and variables even before they start to execute the code.

Example:

const x = 99
one()
function one(){
    let x = 88
    console.log('one executed ', x)
}

The above code snippet has one global and one local execution context, So this is how its memory block would look like:

Variable Value
x undefined
one f(){...}

After the context is established, code execution will take place.

  1. First x is assigned 99 as a value
  2. function one gets executed and a new context is created.

The new context (local context for one) looks like this:

Variable Value
x undefined

After this, execution takes place:

  1. x is assigned 88 as a value
  2. A log will be visible on console : one executed 88.

Call Stack

Its a stack that runs along with execution context and contains current function calls with the Last In First Out (LIFO) principle. After the context is built for a function, it is pushed into the call stack and executed.

Example:

const x = 99
one()
function one(){
    let x = 88
    let y = `one executed ${successMessage()}`
    function successMessage() {
        return 'Successfully!'
    }

    console.log(y)
}

Call stack for this function will go like this:

  1. Firstly, global context is set and pushed to the call stack.
  2. Then, one gets called and its memory block gets formed, then its pushed into call stack and gets executed.
  3. Inside one, there's another function which is called successMessage, has to set up its context first and then jump to the call stack above one(). The reason, one() isn't popped off yet is because its still is providing its scope to successMessage.
  4. After finishing execution successMessage, pops off.
  5. After this, one pops off as well.

This is what the stack looks like :

call stack

Ways to use a function

There are three popular ways to declare a function in javascript

Declaration :

# IIFE (Immediately Invoked Functional Expression)
(function sum() {
    let a = 6
    let b = 8
    console.log(a+b)
})()

# Functional Expression
const sum = function(a, b) {
    return a+b
}

# Functional Statement
function sum (a, b) {
    return a+b
}

# Arrow functions
const sum = (a, b) => {
    return a+b
}

functional statement vs functional expressions

If you know how javascript engine works, you should know that the variables are stored first and then the statements get executed line by line.

So, logically the major difference between function expression and function statement is Hoisting.

Functional expressions are hoisted while the expressions are not. This happens because expressions have been declared already in phase 1 of JS code execution where Javascript stores the values of variables in the global scope. So while saving other variables, it also saves the function. Which explains the example below :

a() // Will print the log.
b() // Will throw the log as it is not executed.


const a = () => {
    console.log('a is exec')
}

function b() {
    console.log('b is exec')
}

More ways to use functions

Anonymous functional Statements

The functions without names (duh). Here is what it looks like:

function(){

}

The above function will throw an error because function statements always require a name. Although they could be used in functional expressions like this:

 const sum = function() {

 }

Named function Expressions

Just like the anonymous function expression, we can have named ones

 const sum = function abc() {

 }

But you can't just call them by their names, no, they're not some Harry potter villain but its just their existence isn't known to JS engine yet. So when you call abc(), it will log you an error. Although you can access the function inside it recursively like this :

 const sum = function abc() {
    console.log(abc)
 }

First Class Functions

Pre Requisite Knowledge : Parameters are local variables inside the function definition and the values that we pass when calling the function, are called arguments.

function Sum(a, b){ //Parameters
    return a+b
}

Sum(2, 4) //Arguments

Coming back to the first class functions, If a function has ability to be passed as a variable or inside another function parameter, its called a first class function.

function prefix() {
  return "Hello, and welcome to ";
}
function greeting(helloMessage, name) {
  console.log(helloMessage() + name);
}
greeting(prefix, "JavaScript!"); // Hello, and welcome to JavaScript!

here, prefix is a first class function.

Higher order functions

Higher order function is a function that takes other smaller functions as an argument, and returns a function as well.

Example: map, filter, etc.

Why do we need it?

When we write multiple functions for multiple operations that have a part of same/similar logic, It causes code repetition and poor readability along with increased execution time. To overcome these problems, we use HOFs.

Example:

<!-- Here is the higher order function to do few operations on circle -->

const radii = [1, 2, 3, 4]

const circleOps = (arr, logic) => {
    let newArr = []
    for (let i = 0; i < arr.length; i++){
        newArr.push(logic(arr[i]))
    }
}

const area = (radius) => {
    return Math.PI * radius * radius
}

const perimeter = (radius) => {
    return Math.PI * 2 * radius
}

<!-- Returns an array of perimeters of given radii -->
console.log(circleOps(arr, perimeter)) 

As we can see above for all the operations that we do on circles, that needs radius, we have written a higher order function.

In future if we need to get perimeter, we could just pass perimeter logic along with the array of radius. This saved us multiple lines of for loops in each logic that could have ended up with bigger code and heavier js payload.

You might notice, how our function resembles with javascript map function which does the similar job.

Higher Order Function for debounce

function debounce(func, delay) {
  let timeoutId;

  return function (...args) {
    const context = this;

    clearTimeout(timeoutId);

    timeoutId = setTimeout(() => {
      func.apply(context, args);
    }, delay);
  };
}

Closures

A function along with its lexical environment is called closure. A lexical environment is a scope that is around that function, for example:

function a() {
    let s = 'Mohit'
    function b() {
        console.log(s) // Mohit
    }
}

For function b, its lexical environment is inside of function a.

Coming back to closures, in previous example, b is a function, and b along with a is a closure.

There are many uses of closure in javascript since it has a unique tendency of remembering its surrounding. It could be used in memoization, currying and more.

Scope Chaining

When a statement can't find a variable in its scope, it tends to go upwards and find it until it reaches to global scope. This tendency of a variable is called scope chaining.

Example :

function a() {
    let s = 'Mohit'
    let x = 77
    return function b() {
        let x = 99
        return function c(){
            console.log(s) // Mohit
            console.log(x) // 99
        }
    }
}

In the first console log, the js engine tries to find the value of s in the execution context of function c, but since there's no such variable, it treads beyond its scope and enters the execution context of function b, same happens and it is forced to move beyond b as well and finally, inside the memory block of the execution context of a, it does find the value of s and logs it.

For the second console log, 99 gets displayed because the order of scope chain is fom close to far, so it finds the closest value of x and logs it.

In the next article we will understand call-apply-bind, currying and more. Stay tuned with flashweb!

GG!