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:
If we understand the phases of functions, we can try to explain lexical scope and closures theoretically first:
Lexical Scope = Inner Function Scope + Outer Function Scope + … + Global Scope
Let’s take a look at the lifecycle of nested functions and understand how the flow works:
Lifecycle of Nested Functions
1. Definition Phase:
2. Execution Phase:
3. Post-Execution:
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 ??