Understanding Lexical Scoping and Closures in JavaScript

Understanding Lexical Scoping and Closures in JavaScript

Today, we are going to dive deep into two fundamental concepts in JavaScript: Lexical Scoping and Closures. These concepts form the backbone of how JavaScript functions interact with their environment, and once you understand them, you'll be able to write more efficient, powerful code.

What is a closure?

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In simple terms, a closure gives you access to the scope of an outer function from within an inner function, even after the outer function has returned.

In JavaScript, closures are created every time a function is created.

Understanding Lexical Scoping

Before we jump into closures, it's important to understand lexical scoping. In JavaScript, lexical scope refers to the fact that the scope of a variable is determined by its position in the source code. Nested functions have access to variables declared in their outer scopes.

function init() {
    let name = "javascript"; 
    function displayName() {
        console.log(name); // outputs: javascript 
    }
    displayName();
}
init();

Here, displayName() has access to the name variable, even though it's declared outside of it. This is lexical scoping in action.

Let's look at another example:

function outer() {
    let username = "javascriptUser";  
    console.log("outer", secret);  // cannot access it here in the parent function
    function inner() {
        let secret = "my123";
        console.log("inner", username); // prints "javascriptUser"
    }
    function innerTwo() {
        console.log("innerTwo", username); // prints "javascriptUser"
        console.log(secret); // cannot access secret here 
    }
    inner();
    innerTwo();
}
outer();

In this example, inner() can access username because “username“ variable is in the outer scope, but innerTwo() can't access secret, as it's defined inside inner().

What is a Closure?

Closures take advantage of lexical scoping. When we return a function from another function, the inner function retains access to its lexical environment, even after the outer function has finished executing.

Here’s an example:

function makeFunc() {
    const name = "Mozilla Firefox";
    function displayName() {
        console.log(name);
    }
    return displayName;
}
const myFunc = makeFunc();
myFunc(); // outputs: Mozilla Firefox

In this example, we are returning the reference of the “displayName” function stored inside the myFunc variable.

This is an interesting case because, in the earlier example, we were executing the function right after their definition, But in this case, we have passed the reference of the function as a return value.

Now we have to think about scoping as the function’s scope remains till it executes and we have executed the outer function after writing “const myFunc = makeFunc();“, and if the function’s scope is gone, then how is the lexical scoping is going to work.

But our code runs properly, let’s understand why

Why does this happen?

Now after the execution of the outer function, its execution context is going to be removed but now here comes the role of memory, as JS has only 2 things:

  1. To store memory

  2. To execute code on a single thread

Now inside myFunc, it not only stores the displayName function but it also stores the outer function inside it (because of lexical scoping).

So the execution context and complete lexical scope is stored inside “myFunc“ and that is why we are getting proper output.

Practical Use Case of Closures

Let’s apply closures to a real-world scenario. Suppose you have two buttons that change the background color of the page when clicked:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Closure and Lexical Scoping</title>
</head>
</body>
<body style="background-color: #313131;">
    <button id="orange">Orange</button>
    <button id="green">Green</button>

    <script>
        document.getElementById("orange").onclick = function() {
            document.body.style.backgroundColor = 'orange';
        }
        document.getElementById("green").onclick = function() {
            document.body.style.backgroundColor = 'green';
        }
    </script>
</html>

This is what our code should be performing:

This works fine, but imagine if you had 500 buttons. Writing separate onclick handlers for each would be inefficient. Instead, we can use closures to simplify the code:

We can make a click handler, and we can write this code inside the script tag

<script>
    function clickHandler(color){
        document.body.style.backgroundColor = ${color};  
    }
    document.getElementById("orange").onclick = clickHandler;  
</script>

But here is a problem in this line of code "document.getElementById("orange").onclick = clickHandler;", we haven't passed a function to it, instead we have passed a reference of a function, and now it won’t work if we click on the button, background color of the body won't change.

also "onclick" requires executable functions, so let's try to change our code a bit

<script>
    function clickHandler(color){
        document.body.style.backgroundColor = ${color};  
    }
    document.getElementById("orange").onclick = clickHandler("orange");  
</script>

Now, if we give it a proper function like this, then the background color will be orange without clicking the button as we have directly called the function.

A solution to this problem:

We have to change the definition of the function that we have created, here we will be using the concept of closure by returning a function.

<script>
    function clickHandler(color){
        return function(){
            document.body.style.backgroundColor = ${color};
       }  
    }
    document.getElementById("orange").onclick = clickHandler("orange");  
</script>

Now, “clickHandler” passes a function to the “onclick” that is being returned after executing the “clickHandler“ function.

But, the access to the color that is passed to “clickHandler” is also available to the function that is being returned? and the answer is “yes“, because of lexical scoping and closure.

Therefore, the complete lexical scope will be passed to the “onclick“ and the code will run smoothly, final code looks like this:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Closure and Lexical Scoping</title>
</head>
</body>
<script>
    function clickHandler(color){
        return function(){
            document.body.style.backgroundColor = ${color};
       }  
    }
    document.getElementById("orange").onclick = clickHandler("orange");  
</script>
</html>

Here, we pass the color to the clickHandler function, which returns a new function that changes the background color. Thanks to closures, the inner function retains access to the color variable, even though the outer function has already executed.

Conclusion

Closures and lexical scoping are crucial concepts in JavaScript that allow functions to retain access to their outer scope, enabling powerful patterns like function factories and encapsulation. Whether you are building event handlers or working with asynchronous operations, understanding closures will significantly improve your coding skills.