Learn Closures by creating a Caching Function using JavaScript Maps.

Learn Closures by creating a Caching Function using JavaScript Maps.

A practical guide to learning and retaining the concept of Closures by understanding and implementing a caching function to optimise applications.

This guide will build upon the concept of closure and will also cover some of the pitfalls fellow developers come across while learning the concept of closures.

After properly learning about closures we will be implementing a memoising function in javascript which will use the concept of closures and upon implementation of which we will be able to apply what we currently learned.

In software development we need to keep on learning, and whatever we do learn most of the time the hard concepts are tough to grasp. So even if we have gone through the topic it can still be possible that we might have not been able to grasp the concept well.

But After a couple of iterations it just sort of clicks in our mind and there we experience an ‘aha!’ moment.

I haven’t had a solid understanding of closure until now. After going through many articles and videos I was someone who was still not able to wrap my head around closures.

“ I just can’t get it, no matter how hard I try, how much content I consume, I simply do not get the concept of closures “. - I used to say this to myself

The problem that I was doing here was that I was not implementing the concept in practical life, I wasn’t using the concept while programming and had not done any of the examples of closures by myself.

I believe this is where most of us go wrong until we realise…And that realisation usually happens during the aftermath 😂.

Enough said let's Dive Right In!!!

Understanding Scopes First.

The portion of code where variables are accessible is called a scope, here are the types of scopes

  1. Global Scope

  2. Block Scope

  3. Functional / Local Scope.

Here is an example of what I am trying to say.

let astrobot = "Neil Armstrong"

function outerScope() {
  let standardbot = "wall-e";
  console.log(standardbot); // wall-e

  if (true) {
    let spacebot = "wall-e-upgraded";
    console.log(spacebot); // wall-e-upgraded
  }
}

console.log(astrobot) // Neil Armstrong
outerScope();
console.log(spacebot); // Reference Error spacebot is not defined

Let me explain what is going on here…

Global Scope

So the astronaut variable is visible that it is globally scoped as it is not inside any function or curly braces {} so this portion of a JavaScript program is generally called the global scope.

💡 Variables declared inside the block scope are globally accessible from inside the program. hence the name global scope.

Block Scope

Now the block scope is portion inside if condition within the curly braces, I have deliberately set the condition to true so that it always runs. but if we access these variables from outside the scope (block scope) that is if we access these variables in the global scope we will receive an error because the variables declared inside the block scope are not present in the global.

Function Scope

Any particular variable defined inside a function is said to be in a function scope, just relate it with the curly braces again, where they start and where they end, include the name of the function and viola there you have it, the variables inside that particular function are said to be in the function’s scope.

Lexical Scope ( important to know. )

So Lexical Scope gives rise to closures. But what is the lexical scope you might ask, so basically, when you can access the variables of an outer function and even a global environment from an inner function then that is said to be lexical scope, it has been named lexical scope because the JavaScript engine determines the scope during the lexing time.

What do closures even mean?

Since we have some idea of behind the scene working of how inner scope, and outer scope work, and which variables are used where, let’s move on to understanding closures.

According to MDN.

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function's scope from an inner function.

But what I can figure out from the above definition is that something encloses something.

Also, ‘something’ here is generally referred to as functions.

Closures give us access to the parent scope of a function and its variables, even if we are calling it from the child function.

The special thing about the closures is that we can still access the scope from the child function even after the parent function has been terminated. let me elaborate on what I just said with the help of an example.

function father(dad) {
  // I am an outer function that logs the following output.
  console.log(`father ${dad} has been summoned`);

  // I am an inner function that logs the following output.
  return function child(son) {
    console.log(`child ${son} son of ${dad} has been summoned`);
  };

// Also notice that the child function is being returned.
}

const callChild = father("John"); // father John has been summoned

callChild("Johnny"); // child Johnny son of John has been summoned

In the above example notice that we have already called the father function inside callChild variable but then in the child function we are still able to access the value of the father function from within the child function even though the father function has already been called.

⚠️ When using closures the variables from the outer scope are not cleaned up or in other words, they are not garbage collected. This can be one of the downsides of having closures.

Closures give rise to Private Variables.

Private Variables are very straightforward to understand we can only access whatever the function returns, this means the variables declared inside the function are not accessible if not being returned.

In the example below we are creating two functions each having its unique variable score.

Here score is a private variable.

The play function can access the score variable due to the lexical scoping. Here the play function is forming a closure with the whichGame function.

Also, note that this method is particularly helpful as we can maintain multiple games at once.

/* 
notice that every time a new function is called the score variable starts with 
zero
Here Javascript is creating a different execution context
everytime a different function is called.
*/

function whichGame(gameName, player) {
  let score = 0; // declaring a private variable

  return function play() {
    score++;
    console.log(`${player} scores ${score} in the game ${gameName}`);
  };
}

const kabaddi = whichGame("Kabaddi", "Rahul");
const volleyball = whichGame("Volleyball", "Aditi");

/* 
Also note that we are not able to access the score variable in any way.
All we can do here is provide the game name and the player name.
*/

kabaddi(); // Rahul scores 1 in the game Kabaddi
kabaddi(); // Rahul scores 2 in the game Kabaddi

volleyball(); // Aditi scores 1 in the game Kabaddi
kabaddi(); // Rahul scores 3 in the game Kabaddi

volleyball(); // Aditi scores 2 in the game Kabaddi
volleyball(); // Aditi scores 3 in the game Kabaddi

To have a rough idea about what closures mean and how they are being used in the memoise function the context above was necessary.

Alright! Alright! Enough chit chat let’s address the elephant in the room…

Creating a Memoise Function using Closures.

What is Caching / Memoising?

It is an optimisation technique that stores the computed results in a cache and retrieves that same information from the cache instead of computing it again, Thereby making the applications more efficient and faster.

In the world of computers computation is a costly process, and it can be avoided if we use efficient caching techniques.

In this example of closure, we will be using Javascript Map() constructor to create a hashmap, this hashmap will have dynamic key-value pairs.

These keys will be storing the operations and their values will be storing the result.

First, let’s start by creating a function that is computation heavy…

We will take the help of recursion obviously. 😂

function printFibonacciRecursively(index) {
  if (index <= 1) {
    return index;
  }

  return (
    printFibonacciRecursively(index - 1) + printFibonacciRecursively(index - 2)
  );
}

console.time();
console.log(printFibonacciRecursively(30));
console.timeEnd();

This is the output that we get :

832040
default: 40.386ms

So 40 milliseconds is a lot if we put the index to be 30.

What if we created a memoise function that would do the computation only once and then store the value in the cache upon coming across the same value then rather than doing the computation again it would return the already cached value.


// Fibonacci function that we created earlier

function printFibonacciRecursively(index) {
  if (index <= 1) {
    return index;
  }

  return (
    printFibonacciRecursively(index - 1) + printFibonacciRecursively(index - 2)
  );
}

// basically a function that shows the runtime
// of a function
function showRunTime(func) {
  console.time();
  console.log(func());
  console.timeEnd();
}

// This is the main part, the memoise function.
function memoise(func) {
    // we use map here because unlike the object we can store dynammic 
        // key in the Map.
    // Also it gives us access to methods such as has, set and get.

  const cache = new Map(); // declaring the private variable cache

  return (limit) => {
    // converting the limit to string for easier access
    const key = limit.toString();

    // simple check that says if the cache has
    // the key and it's value simply return the value
    if (cache.has(key)) return cache.get(key);

    // else run the function recieved and add it to the cache as well
    cache.set(key, func(limit));
    return cache.get(key);
  };
}

const memoiseFn = memoise(printFibonacciRecursively);

showRunTime(() => memoiseFn(30));
showRunTime(() => memoiseFn(30));

showRunTime(() => memoiseFn(40));
showRunTime(() => memoiseFn(40));

showRunTime(() => memoiseFn(44));
showRunTime(() => memoiseFn(44));

And here are the results that we get :

832040
default: 12.685ms
832040
default: 0.049ms
102334155
default: 983.047ms
102334155
default: 0.054ms
701408733 ( All I did here was increase the number 40 to 44 ).
default: 6.860s ( This value literally hogged up my system for 6 seconds 😱 ).
701408733
default: 0.061ms ( Now look at the runtime after being cached ).

So we went from 6 seconds to straight nill milliseconds in the second attempt which is an insane improvement.

So where are closures coming into play in the above-mentioned memoise function you may ask?

  • Look at the private variable cache.

  • We have abstracted all the logic of caching and only returned the value received in the cache.

  • The function received in the outer function is being called in the inner return function. Due to...? Ans : lexical scoping .


    TL ; DR

  1. Scope determines the accessibility of variables in JavaScript and is of three types: Global, Block, and Function Scope.

  2. Lexical scope gives rise to closures, which allow accessing the outer function's scope and variables from an inner function.

  3. Closures allow accessing the parent scope of a function and its variables even if it has been terminated.

  4. A closure is a combination of a function and references to its surrounding state (the lexical environment).

  5. Implementing closures in practical programming is essential to understand the concept well.

  6. Variables from the outer scope are not cleaned up or garbage collected when using closures.


Conclusion

I know that learning about closures can be a challenging process, but as I point out in this guide, the key to mastering this concept is to practice using it in real-life examples. By doing so, fellow developers will be able to apply what they've learned and experience that "aha!" moment where everything finally clicks.

I tried to break down each aspect of closures and provide examples to help fellow developers get a better understanding of how everything works. I think the section on the lexical scope will be particularly helpful, as it clarifies how inner and outer functions work together.

It can be intimidating to dive into a complex topic like closures, but I aim to make it feel like we're just having a conversation about it. I genuinely want to help others understand this topic, and I hope my passion shines through. Hence I have used a friendly and approachable tone.

Overall, I'm excited to publish this guide and recommend it to anyone who wants to learn more about closures. I believe it's packed with helpful information, practical examples, and relevant information. I can't wait to hear from you guys, it would be great if you guys would also share your experiences with closures. Together, we can continue to learn and grow as developers!

You can connect with me on Twitter, LinkedIn, and GitHub

Until next time, Ciao!! 👋