What is JavaScript Closures?
February 13, 2023
Closures are a fundamental concept in programming languages, and their use is widespread in JavaScript. Many important functions in JavaScript libraries utilize closures, including the useState
function in the React library, which is implemented using closures. Understanding not only what closures are, but also their practical applications, is crucial to effectively leveraging their power.
What is a Closure?
A closure is a combination of a function and its lexical environment, or scope. This allows the inner function to access and remember a variable from outside of its scope. This memory retention ability is why closures are often used for state preservation.
Here's a simple example that demonstrates a closure in action. The inner function inner
accesses the variable a from the outer function outer
and retains its value. When inner
is called, it remembers the previous value of a
and increments it.
function outer() {
let a = 0;
function inner() {
a += 1;
console.log(a);
}
return inner;
}
const inner = outer();
inner(); // 1
inner(); // 2
inner(); // 3
Application of Closures
State Preservation
Closures are essential for maintaining the state of a program. The React library, a widely used JavaScript framework, provides a useState
function to handle state management. The code below shows a simplified version of the useState function that leverages closures to retain state in the internal functions getState and setState. The getState function can retrieve the latest updated value, which is a key feature in state management.
// Because of the closure, getState and setState can access and remember the state
function useState(initialState) {
let state = initialState;
function getState() {
return state;
}
function setState(updatedState) {
state = updatedState;
}
return [getState, setState];
}
const [count, setCount] = useState(0);
count(); // 0
setCount(1);
count(); // 1
setCount(500);
count(); // 500
Caching Mechanism
Closures can be utilized to create a caching system. The ability of internal functions to retain references to external variables enables the creation of a cache that can store frequently used data.
The example below demonstrates this concept. The returned arrow function has access to the external cache variable due to closure. This allows us to reuse the cache to store cached values, thus eliminating the need for repetitive calculations.
function cached(fn) {
const cache = {};
// The returned arrow function can access the external cache variable and remember this variable
// Therefore, this cache can be used to store the calculated results
return (...args) => {
// Stringify the input and use it as a key
const key = JSON.stringify(args);
// If the key is already in the cache, there is no need to repeat the calculation, and the previously stored calculation result is returned directly
// If the key is not there yet, after the operation, put the key and the operation result in the cache, so that repeated operations can be avoided in the future
if (key in cache) {
return cache[key];
} else {
const val = fn(...args);
cache[key] = val;
return val;
}
};
}
Emulate Private Variables
Closures can be used to emulate private variables that are inaccessible from the outside. In the code example below, the variable privateCounter
is protected from external modification, but can still be accessed and updated by the functions increment
and decrement
due to closure. This helps prevent accidental changes to privateCounter
by limiting its modification to the specified functions.
This example illustrates how privateCounter
remains hidden and can only be altered through increment
and decrement
.
// privateCounter cannot be modified externally,
// Increment and decrement can be accessed to privateCounter because of the closure
// Therefore, privateCounter can only be changed through increment and decrement, which can effectively avoid being touched by mistake
var counter = (function () {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function () {
changeBy(1);
},
decrement: function () {
changeBy(-1);
},
value: function () {
return privateCounter;
},
};
})();
console.log(counter.value()); // logs 0
counter.increment();
counter.increment();
console.log(counter.value()); // logs 2
counter.decrement();
console.log(counter.value()); // logs 1
Disadvantages of Closures
Closures are a powerful tool in programming, but they can also result in memory leaks if not used correctly. The closure's ability to allow internal functions to retain external variables can cause these variables to persist in memory, leading to memory leaks if the code is executed repeatedly.
The example below demonstrates how a memory leak can occur when using closures. The longArray
constant is still remembered by addNumbers
due to the closure, even if it is not in use. This can cause a buildup of memory over time, resulting in a memory leak.
function outer() {
const longArray = [];
return function inner(num) {
longArray.push(num);
};
}
const addNumbers = outer();
for (let i = 0; i < 100000000; i++) {
addNumbers(i);
}