What is JavaScript Closures?

February 13, 2023

☕️ Support Us
Your support will help us to continue to provide quality content.👉 Buy Me a Coffee

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);
}
☕️ Support Us
Your support will help us to continue to provide quality content.👉 Buy Me a Coffee