DEV Community

Ayako yk
Ayako yk

Posted on

Understanding Closures and Lexical Environment in JavaScript

In JavaScript, the concept of closure is crucial. In a past blog, I discussed it with some example codes. Today, I'll go in-depth into why it is possible and how it works, which involves the Lexical Environment.

Variables
var:

  • It is considered deprecated and is rarely used in modern JavaScript.
  • Variables declared with var are function-scoped, not block-scoped, which can lead to unintended behavior.
  • It declared outside a function, var creates a global variable that can be accessed anywhere, potentially causing bugs.
  • var declarations are also hoisted, but their value is undefined until the assignment is evaluated.

let:

  • Used to declare a variable that can be reassigned.
  • It is block-scoped, meaning it is only accessible within the block or {} where it is declared.

const:

  • Used to declare a variable that cannot be reassigned after its initial value is set.
  • Like let, it is block-scoped.
  • While the reference itself cannot be changed, the contents of objects or arrays declared with const can be modified.

Scope
Block Scope:
In JavaScript, code inside {...} is called a block, and variables declared inside a block with let or const are block-scoped.
These variables are only accessible within the block and not from the outside.

Example:

{
    let user = "John";
    console.log(user); // John
}
console.log(user); // ReferenceError: user is not defined
Enter fullscreen mode Exit fullscreen mode

Nested Functions:
Functions can be nested inside other functions.
Inner functions have access to variables declared in their parent function's scope due to lexical scoping, meaning variables declared in the parent function can be used within the nested function.

Example:
The variables are accessible from the nested function, getFullName()

function sayHiBye(firstName, lastName) { 
    // helper nested function to use the below function 
    getFullName() { 
        return firstName + " " + lastName; 
    } 
    alert( "Hello, " + getFullName() ); 
    alert( "Bye, " + getFullName() ); 
}
Enter fullscreen mode Exit fullscreen mode

The Modern JavaScript Tutorial

Closure
Closures are a fundamental and powerful concept in JavaScript.

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 a function access to its outer scope. In JavaScript, closures are created every time a function is created, at function creation time.
MDN

Closures enable functions to access their outer scope.
They are created at function definition time, not at execution time.
Closures are often used in scenarios like data encapsulation, callbacks, and maintaining state.

I talked about closures with example codes here:
JavaScript Closure

This time, I want the first call to return 10. I'll modify the code as follows:

function countdown(){
    let count = 10; // Initialize count to 10

    return function decrement(){
        return count -= 1; // Decrement and return the current count
    }
}

let counter = countdown();
console.log(counter()); //10
console.log(counter()); // 9
console.log(counter()); // 8

let counter2 = countdown();
console.log(counter2()); // 10
console.log(counter2()); // 9
console.log(counter2()); // 8
Enter fullscreen mode Exit fullscreen mode

Closures allow us to maintain and manipulate state (like count) across multiple calls to a function, enabling features such as encapsulation and stateful behavior.

Lexical Environment
A Lexical Environment is a theoretical construct in JavaScript that represents the environment associated with a running function, code block, or script. It helps manage variable scope and function declarations but cannot be directly accessed or manipulated in code.

The Modern JavaScript Tutorial provides a clear explanation with great illustrations. Let's follow their explanation first.

The Lexical Environment object consists of two parts:

  • Environment Record – an object that stores all local variables as its properties (and some other information like the value of this).
  • A reference to the outer lexical environment, the one associated with the outer code.

When a script begins execution, the global Lexical Environment is created. Variables declared but not initialized are set to undefined. Function Declarations are fully initialized and ready to use, even before their declaration appears in the code.

Image description

When accessing a variable, JavaScript searches in the current (inner) Lexical Environment first and then continues to outer environments until it finds the variable or throws an error (in strict mode).

Image description

When a function is created, it is associated with a Lexical Environment via its internal [[Environment]] property. This reference, set at creation time, ensures that the function remembers the context in which it was defined.

The Modern JavaScript Tutorial

A Detailed Explanation of the countdown() Function
Now, let's explore it using the example of the countdown() function shared above.

When a script starts, a global Lexical Environment is created:

countdown: function  -- (outer) --> null
Enter fullscreen mode Exit fullscreen mode

When a function is called, a new Lexical Environment is created for that function call:

Lexical Environment of countdown() call
  count: 10    
     |
  (outer)
     |
global Lexical Environment  
  countdown: function
  count: undefined
     |
  (outer)
     |
   null  
Enter fullscreen mode Exit fullscreen mode

Here is the countdown() function:

function countdown(){
    let count = 10;

    return function decrement(){
        return count -= 1;
    }
}
Enter fullscreen mode Exit fullscreen mode

When we first call countdown() with let counter = countdown();, the variable count is initialized to 10 instead of 9. count: 10

At this point, countdown.[[Environment]] is created, and countdown() references it. This environment stores the count variable, which is modified each time counter() is called.

let counter = countdown();

console.log(counter()); // 10
console.log(counter()); // 9
console.log(counter()); // 8
Enter fullscreen mode Exit fullscreen mode

Each new call to countdown() creates a new Lexical Environment. When a new counter2 is created, it references a different countdown[[Environment]] property, so it starts at 10 again.

let counter2 = countdown();

console.log(counter2()); // 10
console.log(counter2()); // 9
console.log(counter2()); // 8
Enter fullscreen mode Exit fullscreen mode

When I learned and wrote the blog about closures, I understood them systematically and from a coding perspective. Now that I've learned about the Lexical Environment, it has given me deeper insights and a more thorough understanding.

Top comments (0)