Sep 20, 2016Last modified April 25, 2025

Closures in javascript

Lets start with an analogy.

A closure lets a function remember and access its birth-scope, even when it’s running elsewhere.

A closure is basically a function that has access to the variables declared in the scope of the outer function, even after the outer function has returned / finished executing.

Lexical Scope / environment

When a function is defined inside another function, the inner function has access to the variables declared in the outer function. This is called lexical scope.

Lets see an example.

let globalVar = "I'm global";

function outerFunction() {
let outerVar = "I'm from outer";

function innerFunction() {
console.log(globalVar); // innerFunction can access globalVar
console.log(outerVar); // innerFunction can access outerVar
let innerVar = "I'm from inner";
console.log(innerVar);
}

innerFunction();
}

outerFunction();
let globalVar = "I'm global";

function outerFunction() {
let outerVar = "I'm from outer";

function innerFunction() {
console.log(globalVar); // innerFunction can access globalVar
console.log(outerVar); // innerFunction can access outerVar
let innerVar = "I'm from inner";
console.log(innerVar);
}

innerFunction();
}

outerFunction();

In this example, when innerFunction executes, its lexical environment includes:

  • Its own scope (containing innerVar).
  • The scope of outerFunction (containing outerVar).
  • The global scope (containing globalVar).

Saving state

The real power of closures comes into play when the inner function is returned from the outer function. Even after the outer function has completed its execution and its local variables should theoretically be gone, the inner function remembers and retains access to the outer function's variables (its lexical environment).

function createCounter() {
  let count = 0;

  // this function will remember the count variable value
  function increment() {
    count++;
    console.log(count);
  }

  return increment;
}

const counter = createCounter();
counter(); // Output: 1
counter(); // Output: 2
counter(); // Output: 3

When to use closures

  1. You can use closures to create private variables in JavaScript. By encapsulating variables within a closure, you can prevent them from being accessed or modified from outside the closure.

  2. Closures are useful for creating functions that maintain state. If you noticed, in counter example, the last count value is retained even after the function has returned.

  3. Closures are commonly used in event handlers and callbacks to maintain access to variables from their surrounding scope when the event or asynchronous operation occurs.

function handleClick(elementId) {
const message = `Button with ID ${elementId} clicked!`;
document.getElementById(elementId).addEventListener('click', function() {
console.log(message); // The inner function closes over 'message'
});
}

handleClick('myButton');
function handleClick(elementId) {
const message = `Button with ID ${elementId} clicked!`;
document.getElementById(elementId).addEventListener('click', function() {
console.log(message); // The inner function closes over 'message'
});
}

handleClick('myButton');

Issues with closures

  1. Memory Leaks: Closures can lead to memory leaks if not managed properly. If a closure holds references to large objects or functions, it can prevent those objects from being garbage collected, even if they are no longer needed.

  2. Performance: Closures can have a slight performance impact due to the additional overhead of managing closures. However, in most cases, this impact is negligible and can be optimized by using techniques like function caching or lazy initialization.

Implementing throttle function with closure

A throttle function is a utility function that limits the rate at which a given function can be called.

It takes a function and a time interval as arguments. The function is executed at most once in every time interval.

Since we have to write a function which controls the rate of execution of other function, lets start defining our throttle function first. This function will return the throttled function

function throttle(func, delay){

return () => {
// call func after delay here
}
}
function throttle(func, delay){

return () => {
// call func after delay here
}
}

Now since we have to execute the function after a certain time interval, we can use setTimeout function.

function throttle(func, delay){
let timeout = null
return () => {
if(!timeout){
func();
timeout = setTimeout(()=>{
timeout = null;
}, delay);
}
}
}
function throttle(func, delay){
let timeout = null
return () => {
if(!timeout){
func();
timeout = setTimeout(()=>{
timeout = null;
}, delay);
}
}
}

Try the code in the sandbox below.

    function fun(a,b) {
    console.log(a + ' ' + b);
}

function throttle(func, delay) {
  let timeout = null;
  return function(...args){
    if (!timeout) {
      func.apply(this, args);
      timeout = setTimeout(() => {
        timeout = null;
      }, delay);
    }
  };
}

const throttledFun = throttle(fun, 500);

throttledFun(10, 20); // This will execute immediately
throttledFun(30, 40); // This will be ignored
setTimeout(() => {
  throttledFun(50,60); // This will also be ignored
}, 300);
setTimeout(() => {
  throttledFun(70,80); // This will execute
}, 600);

Implementing debounce function with closure

Debouncing is a technicque where you basically wait for inactivity before executing a function. So you wait for the user to finish the input before executing the function. For example, in a search bar, you want to wait for the user to finish typing before executing the search api call.

Practical Use Cases for Debounce:

  • Search Autocomplete: As mentioned, waiting for the user to stop typing before fetching suggestions reduces unnecessary API calls and improves performance.  
  • Resizing Events: When a user resizes a window, the resize event fires rapidly. Debouncing ensures that expensive layout recalculations or redraws only happen once after the user has finished resizing.  
  • Scroll Events (less common): In some cases, you might want to trigger an action only after the user has stopped scrolling for a bit (though throttling is often a better fit here).
/**
 * @param {Function} func
 * @param {number} wait
 * @return {Function}
 */
export default function debounce(func, wait) {
  let timeout;
  return function (...args) {
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      func.apply(this, args);
    }, wait);
  };
}

Implementing memoize function with closure

Memoization is a technique used to optimize the performance of functions by caching the results of expensive function calls and returning the cached result when the same inputs occur again.

function memoize(func) {
  const cache = new Map();

  return function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      console.log('returning cached result...');
      return cache.get(key);
    }
    const result = func.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

// Example usage:
function add(a, b) {
  console.log('Calculating sum...');
  return a + b;
}

const memoizedAdd = memoize(add);

console.log(memoizedAdd(2, 3)); // Output: Calculating sum... 5
console.log(memoizedAdd(2, 3)); // Output: 5 (from cache)
console.log(memoizedAdd(5, 10)); // Output: Calculating sum... 15
console.log(memoizedAdd(5, 10)); // Output: 15 (from cache)