DeCoding Closures: Exploring JavaScript with Fun Examples
Introduction
JavaScript is primarily a function-oriented language, offering developers a great deal of flexibility. You can create functions on the fly, pass them as arguments, and even call them from completely different parts of your code. This makes JavaScript highly dynamic and powerful.
One of the unique features that makes JavaScript stand out is Closures. Unlike many other languages, JavaScript allows functions to "remember" the variables from their outer scope, even after that scope has finished executing.
But what do we mean by that?
To fully understand closures, we need to explore two important concepts: scope and lexical environment. These lay the foundation for understanding how and why closures work.
In this blog, we’ll break down these basics before diving into the concept of closures itself. Once we’ve covered that, we’ll explore some practical examples to see how closures are used in real-world applications. By the end of this post, you’ll not only understand closures but also feel confident using them in your own projects.
Understanding Scope in JavaScript
When you declare a variable in JavaScript, such as:
let name = "Hardik";
console.log(name); //Output:Hardik
//name is accessible within this file
you can use this variable throughout the file where it’s declared.
This is because of its scope.
Scope: determines where a variable can be accessed in your code.
In this case, name
has global scope within the file, meaning it is accessible from anywhere in that file.
What if we write same code in same file, with in curly braces ?
{
let name = "Hardik";
console.log(name); // Output: Hardik
}
console.log(name); // Error! name is not defined
But Why ?
Here, the variable
name
is declared inside a code block{...}
.Code blocks are sections of code enclosed in curly braces
{...}
.In this example,
name
is only accessible within that block.Trying to access
name
outside the block results in an error.This is because of block scope.
Block scope means that variables declared inside a code block are confined to that block and cannot be accessed outside of it, even if it’s within the same file.
A similar behavior can be seen with functions as well.
function nameScope() {
let name = "Hardik";
console.log(name); // Output: Hardik
}
nameScope();
console.log(name); // Error! name is not defined
name
is declared inside the functionnameScope
.This is an example of local scope.
Variables declared within a function are only accessible within that function.
Scope Summary:
Scope Type | Accessibility |
Global Scope | Accessible from any part of the file. |
Block Scope | Accessible only within the code block {...} . |
Local Scope | Accessible only inside the function. |
What is Lexical Scoping ?
Lexical scope means that the scope of a variable is based on where it is defined in the code.
It remains accessible only within that block of code.
function outerFunction() {
let outerVariable = 'I am outside!';
function innerFunction() {
let innerVariable = 'I am inside!';
console.log(outerVariable); // Can access outerVariable
console.log(innerVariable); // Can access innerVariable
}
innerFunction();
console.log(outerVariable); // Can access outerVariable
console.log(innerVariable); // Cannot access innerVariable (causes an error)
}
outerFunction();
Lexical Scope refers to the way the scope of a variable is determined by its location in the code. In above example:
outerVariable
is declared withinouterFunction
, so it is accessible withinouterFunction
and any functions nested inside it (likeinnerFunction
).innerVariable
is declared withininnerFunction
, so it is accessible only withininnerFunction
.
What is Closure ?
Closure is a concept where a function retains access to variables from its outer (enclosing) scope even after that function has finished executing. In your code:
innerFunction
is a closure because it has access toouterVariable
even thoughouterVariable
is defined in the outer function (outerFunction
).
Definition
A closure is a function that retains access to variables from its outer (enclosing) function even after the outer function has finished executing. Closures are a result of lexical scoping and allow functions to maintain a reference to their environment.
function createCounter() {
var count = 0;
function incrementCounter() {
count += 1;
console.log(count);
}
return incrementCounter;
}
var counter = createCounter();
counter(); // Output: 1
counter(); // Output: 2
Explanation:
createCounter
defines a local variablecount
and an inner functionincrementCounter
.incrementCounter
forms a closure overcount
, meaning it retains access tocount
even aftercreateCounter
has finished executing.Each time
counter()
is called, it updates and logs the value ofcount
because the closure maintains a reference to the originalcount
variable.
Updating Outer Scope Variables
Retains Reference: Closures retain a reference to the outer variables, not just a copy of their values.
Dynamic Updates: If an outer variable is updated after the closure is created, the closure reflects these updates because it maintains a reference to the variable.
function createCounter(initialValue) {
var count = initialValue;
function getCount() {
console.log(count);
}
count = 100; // Modifying outer variable
return getCount;
}
var counter = createCounter(10);
counter(); // Output: 100
Explanation:
createCounter
initializescount
withinitialValue
and defines thegetCount
function.After defining
getCount
, the outer variablecount
is updated to 100.The closure formed by
getCount
retains access to the updated value ofcount
. As a result, whencounter()
is called, it reflects the modified value ofcount
.
Shadowing with Closures
Variable shadowing happens when a nested function defines a variable with the same name as a variable in an outer scope.
The inner variable shadows the outer one within its scope.
function createCounter() {
var count = 0;
function increment() {
var count = 10; // Shadowing outer `count`
console.log(count); // Output: 10
}
increment();
console.log(count); // Output: 0
}
createCounter();
Explanation:
createCounter
defines a variablecount
and an inner functionincrement
.increment
defines its owncount
which shadows the outercount
.Within
increment
, the innercount
is accessed, while outside it, the outercount
is accessed.
Practical Frontend Techniques
Currying
Currying is a functional programming technique where a function that takes multiple arguments is transformed into a series of functions that each take a single argument.
Currying utilizes closures to retain the state of partially applied arguments. Each curried function call returns a new function that maintains access to the previously provided arguments, allowing for incremental function application.
Read more: here
// Curried function to create a greeting message
function greet(formality) {
return function(timeOfDay) {
return function(name) {
return `${formality} ${name}, good ${timeOfDay}!`;
};
};
}
// Usage
const formalGreeting = greet('Good evening'); // Curried function with formality
const eveningGreeting = formalGreeting('evening'); // Curried function with time of day
const message = eveningGreeting('Alice'); // Complete the greeting with the name
console.log(message); // Output: Good evening Alice, good evening!
Debouncing
Debouncing is a technique used to ensure that a function is only executed after a certain period of inactivity or delay. This is particularly useful for handling events that fire frequently, such as keystrokes or window resizing, to prevent excessive function calls and improve performance.
Under the hood, debouncing often uses closures to maintain the state of the timer. The closure keeps a reference to the timeout function and manages its execution, ensuring that the function is called only after the specified delay has passed since the last event.
function debounce(func, delay) {
let timer; // Closure variable to keep track of the timer
return function(...args) {
clearTimeout(timer); // Clear previous timer
timer = setTimeout(() => func(...args), delay); // Set a new timer
};
}
// Usage
const debouncedFunction = debounce(() => console.log('Debounced!'), 300);
window.addEventListener('resize', debouncedFunction);
Conclusion
Closures and lexical scoping in JavaScript are essential concepts that enhance coding flexibility and power. Lexical scoping determines a variable's scope based on its location in the code. Closures allow a function to retain access to variables from its outer scope even after the outer function has finished executing. Practical applications include currying and debouncing, which utilize closures to manage state and improve performance.
Subscribe to my newsletter
Read articles from Hardik Dhamija directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Hardik Dhamija
Hardik Dhamija
I’m a frontend developer with a strong focus on React, TypeScript, and Next.js, currently expanding my skills into full-stack development with Node.js. My goal is to create impactful web solutions and collaborate on innovative projects.