Closure is a very important and interesting concept in JavaScript which a programmer must learn in order to write better readable and maintainable code in JavaScript. In order to understand how Closure works we must know about Lexical Scoping and what does it do. So let’s start our blog by understanding what does scoping means
Scoping
In any programming language scoping of a variable or function means that from what part(s) of the program the variable and function can be accessed. There are mostly two types of scoping used by any programming langue namely :
Lexical Scoping
Dynamic Scoping
Most of the programming languages widely use lexical scoping but Dynamic scoping is used by very few programming languages . We will discuss here about Lexical Scope in more detail as that is our scope of interest
Lexical Scope
Lexical scope refers to the principle that variable access is determined by the placement of variables and blocks in the source code during the lexical analysis phase (also known as the compilation phase, even though JavaScript is often considered an interpreted language). JavaScript uses a lexical scope chain, which means that within a nested function, the inner function has access to its own scope (local variables), the scope of the outer function (including parameters), and the global scope.
What is Closure?
At its core, a closure is a feature in JavaScript where an inner function has access to the outer (enclosing) function’s variables—this includes variables, parameters, and even the outer function’s parameters. This access is maintained even after the outer function has finished executing. In simpler terms, a closure allows a function to “remember” and access its lexical scope even when that function is executed outside that scope.
Creating our first Closure
Let’s create our first example of a closure
function welcome(name){
const place = "XYZ Hotel";
function greetings(){
console.log(`Hi ${name} welcome to ${place}`);
}
return greetings;
}
const welcomeGreetings = welcome("Jane");
welcomeGreetings() // Hi Jane welcome to XYZ Hotel
Here as you can see in the above example, welcome
function is taking a parameter name
and inside the function there is also a constant variable place . Now we also have a inner function greetings which only contains a console.log that prints a message by accessing the variable place and the parameter to the outer function name and then this inner function greetings gets returned from the outer function welcome. We are catching result in a variable welcomeGreetings and when it is called it is printing the message in console. So from above we can see that the inner function greetings() forms a closure and have access to the outer function’s variable and parameters . Even though it is getting executed outside of it’s scope still it has access to those variables and parameters of the outer function.
Some Usage of Closure
Creating Private Access
Closures can be used to create private variables within a function scope, which are inaccessible from outside the function:
function counter() {
let count = 0; // Private variable
function increment() {
count++;
console.log(count);
}
return increment;
}
const counter1 = counter();
counter1(); // Output: 1
counter1(); // Output: 2
// Each closure maintains its own private count variable
const counter2 = counter();
counter2(); // Output: 1
Here as you can see the count variable can’t be accessed directly in order to increment the variable it self. Instead a closure increment() is called which is creating an encapsulation around the count variable and making it private.
Creating Function Factories
Closures can be used to create multiple functions with shared behavior but different initial states or configurations as below
function createMultiplier(multiplier) {
return function(number) {
return number * multiplier;
};
}
const double = createMultiplier(2);
console.log(double(3)); // Output: 6
const triple = createMultiplier(3);
console.log(triple(3)); // Output: 9
Callbacks in Event Handlers
Closures are often used in event handlers to maintain access to variables within their lexical scope:
function greet(name) {
return function() {
console.log(`Hello, ${name}!`);
};
}
const greetJohn = greet('John');
const greetJane = greet('Jane');
greetJohn(); // Output: Hello, John!
greetJane(); // Output: Hello, Jane!
Implementing the Module Pattern
Closures are instrumental in implementing the module pattern, where functions and variables can be kept private and only a public API is exposed:
const calculator = (function() {
let result = 0;
function add(num) {
result += num;
}
function subtract(num) {
result -= num;
}
function getResult() {
return result;
}
return {
add,
subtract,
getResult
};
})();
calculator.add(5);
calculator.subtract(3);
console.log(calculator.getResult()); // Output: 2
Here, an immediately-invoked function expression (IIFE) returns an object with methods (add
, subtract
, getResult
) that manipulate and access result
via closures. result
is private to the module, ensuring its state is controlled and encapsulated.
Memory Management and Closures
One consideration when using closures is memory management. Because closures retain references to their outer function’s variables, they can prevent those variables from being garbage collected even when they are no longer needed. This situation, known as a memory leak, can occur if closures are not managed carefully, particularly when dealing with long-lived closures or large amounts of data.
To mitigate potential memory issues with closures, it’s crucial to be mindful of the data being captured and retained by closures and to ensure that unnecessary closures are released when they are no longer needed.
Best Practices for Memory Management:
Avoid Keeping Unnecessary References: Be mindful of what variables are captured in closures. If a closure captures large objects or arrays unnecessarily, it can lead to increased memory usage.
Release References: Explicitly set variables that are no longer needed to null
when they are no longer needed, especially when closures are involved. This helps in releasing memory sooner than relying solely on garbage collection.
Use Local Variables: Whenever possible, use local variables inside functions instead of relying on closures for variables that are not actually needed outside the function.
Careful with Event Handlers: Event listeners often involve closures. Ensure to remove event listeners when they are no longer needed to prevent closures from retaining unnecessary references.
Performance Considerations: While closures are powerful, excessive use in performance-critical sections of code can impact performance due to memory retention and slower garbage collection.
Closures are a fundamental concept in JavaScript that empowers developers to write more expressive, flexible, and efficient code. By understanding how closures work, leveraging their benefits, and applying best practices, you can harness the full potential of JavaScript’s functional capabilities.
Whether you’re writing simple scripts or complex applications, mastering closures will undoubtedly elevate your proficiency in JavaScript programming. As you continue to explore JavaScript development, keep experimenting with closures to discover new ways they can enhance your code and solve challenging problems effectively.