JavaScript closures are a fundamental concept that every developer should understand. They can seem tricky at first, but once you grasp how they work, you'll find them to be a powerful feature of the language. In this article, we'll explore what closures are, how they work, and provide some practical examples to illustrate their use.
What is a Closure?
A closure is a feature in JavaScript where an inner function has access to the outer (enclosing) function’s variables, even after the outer function has finished executing. Closures are created every time a function is created, at function creation time.
In simpler terms, a closure is formed when a function retains access to its lexical scope, even when the function is executed outside that scope.
Lexical Scope
To understand closures, it is important to grasp the concept of lexical scope. Lexical scope means that a function's scope is determined by its physical location in the code. The scope is fixed at the time of writing and does not change when the function is called
Here's a basic example to illustrate lexical scope:
function outerFunction() {
const outerVariable = 'I am outside!';
function innerFunction() {
console.log(outerVariable);
}
innerFunction();
}
outerFunction(); // Logs: 'I am outside!'
In this example, innerFunction has access to outerVariable because it is defined within the same lexical scope. But what happens when innerFunction is executed outside of outerFunction?
function outerFunction() {
const outerVariable = 'I am outside!';
function innerFunction() {
console.log(outerVariable);
}
return innerFunction;
}
const myInnerFunction = outerFunction();
myInnerFunction(); // Logs: 'I am outside!'
Here, outerFunction returns innerFunction, which is then stored in the variable myInnerFunction. Even though outerFunction has finished executing, innerFunction still has access to outerVariable because of the closure.
How Closures Work
When a function is declared, it retains references to its surrounding scope. Even if the outer function has completed execution, the inner function can still access the variables of the outer function.
function createCounter() {
let count = 0;
return function() {
count += 1;
return count;
};
}
const counter = createCounter();
console.log(counter()); // Output: 1
console.log(counter()); // Output: 2
console.log(counter()); // Output: 3
Use Cases of Closures
1. Data Privacy/Encapsulation by closures
Closures are often used to create private variables that are not accessible from the global scope.
function createPerson(name) {
let age = 0;
return {
getName: function() {
return name;
},
getAge: function() {
return age;
},
incrementAge: function() {
age += 1;
}
};
}
const person = createPerson('John');
console.log(person.getName()); // Output: John
console.log(person.getAge()); // Output: 0
person.incrementAge();
console.log(person.getAge()); // Output: 1
In this example, age is a private variable, and it can only be modified using the methods provided.
2. Function Factories by Closures
Closures can be used to create function factories—functions that create other functions with pre-configured behavior.
function createGreeting(greeting) {
return function(name) {
return `${greeting}, ${name}!`;
};
}
const sayHello = createGreeting('Hello');
const sayHi = createGreeting('Hi');
console.log(sayHello('Alice')); // Hello, Alice!
console.log(sayHi('Bob')); // Hi, Bob!
In this example, createGreeting creates a new function with a specific greeting. The returned function retains access to the greeting variable, thanks to the closure.
3. Partial Application
Partial application allows you to fix a number of arguments of a function and produce a new function.
function add(a) {
return function(b) {
return a + b;
};
}
const addFive = add(5);
console.log(addFive(3)); // Output: 8
console.log(addFive(10)); // Output: 15
4. Memoization with Closures
Closures are useful in memoization, where the result of a function call is cached for future use.
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (cache[key]) {
return cache[key];
}
const result = fn(...args);
cache[key] = result;
return result;
};
}
const factorial = memoize(function(n) {
if (n === 0) return 1;
return n * factorial(n - 1);
});
console.log(factorial(5)); // Output: 120
console.log(factorial(5)); // Output: 120 (cached result)
Closures Common Pitfalls and Best Practices
1. Unintended Closures
Be mindful of closures within loops, as they may lead to unexpected behavior.
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// Output after 1 second: 3, 3, 3
// Using let instead of var fixes this issue because let has block scope.
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// Output after 1 second: 0, 1, 2
2. Memory Leaks because of closures
Be cautious of creating closures that retain references to large objects, which can lead to memory leaks. Ensure that closures do not unnecessarily retain references to large objects when they are no longer needed.
function createBigArray() {
const largeArray = new Array(1000000).fill('test');
return function() {
console.log(largeArray[0]);
};
}
const arrayLogger = createBigArray();
Conclusion
Closures are a powerful feature in JavaScript that allow functions to retain access to their lexical scope even when executed outside of it. They are useful for data privacy, creating function factories, and managing state in asynchronous code.
Top comments (2)
In other words, every time a function is created - as ALL functions retain access to their lexical scope.
Agree,
@jonrandy If you're interested, I have a post about Lexical Scope that you might like to review.
dev.to/rahulvijayvergiya/comparing...