Learn in 5 Minutes: Understanding Lexical Scope and Closures in JavaScript

Welcome to the first part of my “Learn in 5 Minutes” series. ?? In this article, we will dive into one of the trickiest concepts in JavaScript: Lexical Scope and Closures. By the end of these 5 minutes, you’ll have a clear understanding of these foundational topics. We’ll first understand what are they and then explore what they provide us. Let’s jump right in!

What are Lexical Scope and Closure?

Have you ever felt like you ALMOST understood something, like a part of the puzzle was missing? And then, when you finally solve that part, it clicks into place and suddenly the whole machine starts to work? Yeah, it was the deal for me about the lexical scope and closures.

First of all, we should understand there are 2 phases for functions because understanding this is essential for this topic:

  • Definition phase: The function’s name, parameters, and body (the code inside the function) are specified basically the function is defined.
  • Execution phase: During execution, the statements inside the function are processed according to the flow of control, and any variables defined within the function are allocated memory, and their values are computed.

If we understand the phases of functions, we can try to explain lexical scope and closures theoretically first:

  • Lexical scope is the data cluster area (scope) that a function has access to, which includes the function’s own scope and its outer functions’ scopes up to the global scope, determined at the definition phase. We can basically say:

Lexical Scope = Inner Function Scope + Outer Function Scope + … + Global Scope

  • Closure is a concept that defines an inner function’s ability to retain access to its lexical environment. This access is not only established during the function’s definition phase but also persists during the execution phase and even post-execution. This is achieved by forming a closure, which involves capturing an instance of the execution context.

Let’s take a look at the lifecycle of nested functions and understand how the flow works:

Lifecycle of Nested Functions

1. Definition Phase:

  • When an inner function is defined inside an outer function, its lexical scope is established.
  • This means the inner function is aware of and has access to all variables and functions in its outer scopes, up to the global scope.

2. Execution Phase:

  • When the outer function is executed, a new execution context is created. This context includes variables, objects, and the scope chain.
  • The inner function forms a closure by capturing the instance of the execution context of the outer function.
  • The inner function retains its access to the lexical scope via this closure.

3. Post-Execution:

  • Even after the outer function has returned, the inner function retains access to its lexical scope via the closure by preserving the closure in the memory.

Code example

Here’s a code example to illustrate the concepts:

function outerFunction() {
  const outerVar = 'outer variable';

  function innerFunction() {
    const innerVar = 'inner variable';
    console.log(innerVar); // Can access innerVar within innerFunction's scope
    console.log(outerVar); // Can access outerVar from outerFunction's scope
  }

  innerFunction();
  console.log(outerVar); // Can access outerVar within outerFunction's scope
}

outerFunction();        

You can play with this code and try to see the inner function’s access to its lexical scope.

A Metaphor for Clarity

Imagine Lexical scope as a relationship/marriage’s boundaries when it is defined at first as “a relationship”. It extends beyond just you and your partner but it also includes the outer world’s scope (for example your families and growing conditions ) because it is where yours and your relationship’s birth area. And inside your relationship, you have access to that lexical scope area in the starting phase.

During your relationship/marriage, which is the execution phase ?? , you not only get affected by your relationship area but also remain affected by the outer world and you retain access to it during the relationship. This is the closure.

The only difference is that; in a relationship, this might be an inevitable side effect but on JS this is a powerful ability for inner functions given by JS which lets us use closures.

What Does Lexical Scope and Closure Provide Us?

Since we successfully understood what lexical scope and closure concepts are, now we can address the real point of what they provide us. Otherwise, we’d be like parrots, simply memorizing and repeating without comprehension.

Lexical Scopes

With the lexical scopes in JavaScript, when we define nested functions, we don’t need to manually drill information between nested functions or bind instances at every level. For example, in a functional component that includes many variables such as state, props, and DOM elements, accessing these variables at each level of nesting would require binding them to inner functions or passing them as arguments to inner functions. This would lead to unmanageable and chaotic code.

Closures

Closures give inner functions the ability to retain access to their lexical scope even after the outer function has finished executing. Without this ability, managing nested functions would be difficult. Consider common scenarios such as adding event listeners inside another function or higher the bid and thinking of creating function factories (like reducers), let's play bigger and think about handling asynchronous code, setting component state, and even fancy and cooler parts of functional programming: Higher-Order Functions!

Here’s a deeper dive into some of these use cases, with code examples to make the concepts more tangible:

1. Event Handlers

function setupClickHandler(element) {
    let count = 0;

    function handleClick() {
        count += 1;
        console.log(`Button clicked ${count} times`);
    }

    element.addEventListener('click', handleClick);
}

const button = document.getElementById('myButton');
setupClickHandler(button);        

In this example, handleClick is an instance method that has access to the count variable from its outer function due to lexical scope and closures.

2. Function Factories (React Reducer Example)

const initialState = { count: 0 };

function reducer(state, action) {
    switch (action.type) {
        case 'increment':
            return { count: state.count + 1 };
        case 'decrement':
            return { count: state.count - 1 };
        default:
            throw new Error('Unknown action type');
    }
}        

In this example, the reducer function retains access to its state and action parameters each time it is invoked, which is enabled by closures.

3. Handling Async Data

import React, { useState, useEffect } from 'react';

function UserList() {
    const [users, setUsers] = useState([]);

    useEffect(() => {
        async function fetchUsers() {
            const response = await fetch('https://api.example.com/users');
            const data = await response.json();
            setUsers(data);
        }
        fetchUsers();
    }, []);

    return (
        <ul>
            {users.map(user => (
                <li key={user.id}>{user.name}</li>
            ))}
        </ul>
    );
}
export default UserList;        

In this example, the fetchUsers function retains access to the setUsers state setter from the component's lexical scope, allowing it to update the state after the async operation completes.

4. Higher-Order Functions

function map(array, fn) {
    const result = [];
    for (let i = 0; i < array.length; i++) {
        result.push(fn(array[i]));
    }
    return result;
}
const numbers = [1, 2, 3, 4];
const doubled = map(numbers, function(n) {
    return n * 2;
});
console.log(doubled); // Outputs: [2, 4, 6, 8]        

Without closures, the callback function passed to map would not have access to the array elements during the iteration.

Conclusion

Lexical scope and closures provide powerful mechanisms for managing scope, state, and function behavior in JavaScript. They enable encapsulation, simplify state management, and are essential for asynchronous programming and functional programming patterns. Understanding and utilizing these concepts allows developers to write more maintainable and efficient code.

Thank you for reading the article till the end, wish you a fun and sunny day ??

要查看或添加评论,请登录

社区洞察

其他会员也浏览了