The Kraken's Lair: A JavaScript Closure Blog

The Kraken's Lair: A JavaScript Closure Blog

Unlocking the Secrets of JavaScript Closures in The Kraken's Lair

ยท

6 min read

Introduction

The closure is the most esoteric concept of JavaScript. Many JavaScript design patterns including the module pattern use closure. We can build iterators, handle partial applications and maintain a state in an asynchronous world.

But before diving into the concept of closure, let's again take a look at how the function works,

function multiplyBy2(input){
    const result = input*2;
    return result;
}

// New memory gets created during both function calls in their own execution context 
const output = multiplyBy2(7);
const newOutput = multiplyBy2(10);

console.log(output);    // 14
console.log(newOutput);    // 20

When a function gets invoked a new execution context gets created with its own local memory and after the execution, this memory gets deleted. And it doesn't remember the previous running of function and data stored in local memory.

Functions with Memories

When our function gets invoked, we create a live store of data for that function's execution context. Another name for local memory is Variable Environment. When execution is finished local memory gets deleted except the returned value.

But what if our functions can hold on to live data between executions? Something like persistent memory. Let's keep in mind we can return a function from another function.

Returning Function

Let's create a function that can return another function as a value.

function createFunction(){
    function multiplyBy2(num){
        return num*2;
    }

    return multiplyBy2;
}

const generatedFunc = createFunction();
const result = generatedFunc(3);

console.log(result);    // 6

Let's see the thread of execution:

  • Line 1: We are defining a function called createFunction and the code of the function is bundled up and stored in the global memory.

  • Move to Line 8: We declared a constant named generatedFunc and assign it the value returned by the function call.

  • Line 9: We declared a constant named result and assigned it the value returned by generatedFunc.

  • Line 11: We are outputting the returned value in the web console.

The execution context gets deleted as soon as the return keyword is encountered. We can return a function from another function because JavaScript functions are first-class objects. Now, back to our program.

In line 12, we are declaring a constant named result and assign it the value returned by calling generatedFunc.

This function has no relationship with createFunction function. generatedFunc contains returned value from createFunction but it is not related to createFunction.

Nested Function Scope

A nested function can inherit the arguments and variables of its containing function. The inner function contains the scope of the outer function. Let's see an example,

function outer(){
    let counter = 0;

    function incrementCounter(){
        counter++;
        console.log(counter);
    }
    incrementCounter();
}

outer();    // 1
outer();    // 1
outer();    // 1
outer();    // 1

Line 1: We are declaring a function named outer in the global context.

Move to Line 11: We are calling outer function 4 times.

But we can see every time we invoke outer the function, the counter gets incremented by 1 and outputs 1 to the web console. This is because every time function is called a new execution context gets created with new local memory.

The below figure shows the thread of execution,

Retaining Function Memory

We can also create functions that can retain the memory of its execution context. Let's modify the above example a bit.

function outer(){
    let counter = 0;

    // function to increment counter
    function incrementCounter(){
       counter++;
        console.log(counter);
    }
    return incrementCounter;    // returning a function
}

const myNewFunction = outer();
myNewFunction();    // 1
myNewFunction();    // 2

In the above program, we are also incrementing the counter variable but we are returning the incrementCounter function from outer function instead of calling it.

Now, let's keep some points in mind:

  • When function A gets returned from function B as a value, there is no relationship between A and B.

  • When we invoke a function, this function is pushed onto the call stack and a brand new execution context is created with its local memory.

  • Local memory gets destroyed when the execution is finished and we cannot access it again.

So, now the big question is counter variable is in the local memory of outer function and incrementCounter function is being used to increment this variable. But if outer function has no relationship with incrementCounter function then how can we still able to increment this counter?

Now, this is where the concept of Closure comes in!

In the next line, we are invoking myNewFunction.

The counter variable is missing from both the local and global memory. So, we should get some kind of error in our terminal. But wait, it's still working and we are still getting output in our web console.

This is because when we returned incrementCounter function from outer function, we got more than the function definition. It took all its surrounding data present in its local memory and ships it.

This additional data is present in something we are going to call a "Backpack". So, now we have a function definition and also the associated local memory.

But how the function grabs the surrounding data and get it in the bag and ship with it? JavaScript gets a hidden property that links to where all this surrounding data is stored, this property is called [[scope]] .

We cannot access this hidden property. The only way to access this data is to run this function, look for something that is not in its local memory then look for it in this hidden data block.

myNewFunction can access counter variable present in the backpack. The data present in the backpack is persistent. This is how memory-retaining functions can be built.

Closure Technical Definition

A closure is the combination of a function bundled together with references to its surrounding state. The local memory is also called Variable Environment. Let's see some other names for our backpack:

  • Closed over variable environment code

  • Persistent Lexical Scope Reference Data

JavaScript is a lexical scope or static scope language. In other words, where I save my function determines for the life of that function, what data it gets whenever it runs, whatever new label it gets, and what data will it have access to when the function runs.

Practical Applications

Some applications of closure are:

  • Closure gives us memory-persistent functions.

  • When can build helper functions like once and memoize.

  • We can build iterators and generators which use lexical scoping and closure to achieve contemporary patterns for handling data in JavaScript.

  • Module Pattern which helps in preserving the state for the life of an application without polluting the global namespace.

  • Asynchronous JavaScript

Conclusion

In this blog, we have seen

  • How to return a function from another function

  • How nested function scope works

  • How we can build memory-persistent functions using the concept of closure

  • Some practical applications of closure

If you're having any doubts please mention them in the comments below! ๐Ÿ˜Ž

References

ย