JavaScript Closures - Complete Guide

Closures are one of the most powerful features in JavaScript. This guide explains closures, how they work, and practical applications.

What is a Closure? #

A closure is a function that has access to variables in its outer (enclosing) scope, even after the outer function has returned.

function outer() {
  const message = "Hello";

  function inner() {
    console.log(message);  // Can access outer variable
  }

  return inner;
}

const myFunc = outer();
myFunc();  // "Hello" - closure remembers 'message'

How Closures Work #

function makeCounter() {
  let count = 0;  // Private variable

  return function() {
    count++;
    return count;
  };
}

const counter = makeCounter();
console.log(counter());  // 1
console.log(counter());  // 2
console.log(counter());  // 3

// Each call remembers previous count

Lexical Scoping #

Closures work because of lexical scoping - functions have access to variables in their outer scope.

function outer() {
  const x = 10;

  function middle() {
    const y = 20;

    function inner() {
      console.log(x + y);  // Can access both x and y
    }

    return inner;
  }

  return middle();
}

const func = outer();
func();  // 30

Practical Examples #

Private Variables #

function createPerson(name) {
  let age = 0;  // Private

  return {
    getName() {
      return name;
    },

    getAge() {
      return age;
    },

    growOlder() {
      age++;
    }
  };
}

const person = createPerson("Alice");
console.log(person.getName());  // "Alice"
person.growOlder();
console.log(person.getAge());  // 1
// console.log(person.age);  // undefined - private!

Counter with Multiple Operations #

function createCounter(initial = 0) {
  let count = initial;

  return {
    increment() {
      count++;
      return count;
    },

    decrement() {
      count--;
      return count;
    },

    reset() {
      count = initial;
      return count;
    },

    getValue() {
      return count;
    }
  };
}

const counter = createCounter(10);
counter.increment();  // 11
counter.increment();  // 12
counter.decrement();  // 11
counter.reset();      // 10

Function Factory #

function multiply(multiplier) {
  return function(number) {
    return number * multiplier;
  };
}

const double = multiply(2);
const triple = multiply(3);

console.log(double(5));  // 10
console.log(triple(5));  // 15

Event Handlers #

function createButton(label) {
  let clickCount = 0;

  return {
    getLabel() {
      return label;
    },

    click() {
      clickCount++;
      console.log(`${label} clicked ${clickCount} times`);
    }
  };
}

const submitButton = createButton("Submit");
submitButton.click();  // "Submit clicked 1 times"
submitButton.click();  // "Submit clicked 2 times"

Module Pattern #

const calculator = (function() {
  let result = 0;  // Private

  return {
    add(n) {
      result += n;
      return this;
    },

    subtract(n) {
      result -= n;
      return this;
    },

    multiply(n) {
      result *= n;
      return this;
    },

    getResult() {
      return result;
    },

    clear() {
      result = 0;
      return this;
    }
  };
})();

calculator
  .add(5)
  .multiply(2)
  .subtract(3);

console.log(calculator.getResult());  // 7

Memoization #

function memoize(fn) {
  const cache = {};

  return function(...args) {
    const key = JSON.stringify(args);

    if (key in cache) {
      console.log("From cache");
      return cache[key];
    }

    console.log("Calculating");
    const result = fn(...args);
    cache[key] = result;
    return result;
  };
}

const fibonacci = memoize(function(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
});

console.log(fibonacci(10));  // Calculates
console.log(fibonacci(10));  // From cache

Partial Application #

function partial(fn, ...args) {
  return function(...moreArgs) {
    return fn(...args, ...moreArgs);
  };
}

function greet(greeting, name) {
  return `${greeting}, ${name}!`;
}

const sayHello = partial(greet, "Hello");
console.log(sayHello("Alice"));  // "Hello, Alice!"

const sayGoodbye = partial(greet, "Goodbye");
console.log(sayGoodbye("Bob"));  // "Goodbye, Bob!"

Curry Function #

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn(...args);
    }

    return function(...moreArgs) {
      return curried(...args, ...moreArgs);
    };
  };
}

function add(a, b, c) {
  return a + b + c;
}

const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3));  // 6
console.log(curriedAdd(1, 2)(3));  // 6
console.log(curriedAdd(1)(2, 3));  // 6

Common Pitfalls #

Loop and Closures #

Problem:

for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i);  // 3, 3, 3
  }, 1000);
}

Solution 1: IIFE

for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(function() {
      console.log(j);  // 0, 1, 2
    }, 1000);
  })(i);
}

Solution 2: let

for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i);  // 0, 1, 2
  }, 1000);
}

Memory Leaks #

function attachHandler() {
  const element = document.getElementById('button');
  const data = new Array(1000000);  // Large data

  // Closure keeps reference to data
  element.addEventListener('click', function() {
    console.log(data.length);
  });
}

// Better: only keep what you need
function attachHandler() {
  const element = document.getElementById('button');
  const dataLength = new Array(1000000).length;

  element.addEventListener('click', function() {
    console.log(dataLength);
  });
}

Real-World Applications #

Debounce Function #

function debounce(fn, delay) {
  let timeoutId;

  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      fn(...args);
    }, delay);
  };
}

const search = debounce(function(query) {
  console.log("Searching for:", query);
}, 500);

// Only last call after 500ms executes
search("a");
search("ab");
search("abc");  // Only this executes

Throttle Function #

function throttle(fn, delay) {
  let lastCall = 0;

  return function(...args) {
    const now = Date.now();

    if (now - lastCall >= delay) {
      lastCall = now;
      fn(...args);
    }
  };
}

const handleScroll = throttle(function() {
  console.log("Scrolled");
}, 1000);

window.addEventListener('scroll', handleScroll);

Once Function #

function once(fn) {
  let called = false;
  let result;

  return function(...args) {
    if (!called) {
      called = true;
      result = fn(...args);
    }
    return result;
  };
}

const initialize = once(function() {
  console.log("Initializing");
  return "Initialized";
});

initialize();  // "Initializing", returns "Initialized"
initialize();  // Returns "Initialized" (doesn't log)

Rate Limiter #

function createRateLimiter(maxCalls, timeWindow) {
  const calls = [];

  return function(fn) {
    const now = Date.now();

    // Remove old calls
    while (calls.length > 0 && calls[0] < now - timeWindow) {
      calls.shift();
    }

    if (calls.length < maxCalls) {
      calls.push(now);
      return fn();
    } else {
      throw new Error("Rate limit exceeded");
    }
  };
}

const limiter = createRateLimiter(3, 1000);  // 3 calls per second

limiter(() => console.log("Call 1"));
limiter(() => console.log("Call 2"));
limiter(() => console.log("Call 3"));
// limiter(() => console.log("Call 4"));  // Error

Closure Interview Questions #

Question 1: What will this log? #

function test() {
  for (var i = 0; i < 3; i++) {
    setTimeout(function() {
      console.log(i);
    }, i * 1000);
  }
}

test();
// Answer: 3, 3, 3 (all reference same 'i')

Question 2: Fix the code #

function createFunctions() {
  const funcs = [];

  for (var i = 0; i < 3; i++) {
    funcs.push(function() {
      return i;
    });
  }

  return funcs;
}

// Fix:
function createFunctions() {
  const funcs = [];

  for (let i = 0; i < 3; i++) {
    funcs.push(function() {
      return i;
    });
  }

  return funcs;
}

Best Practices #

  1. Understand scope - Know what variables are captured
  2. Avoid memory leaks - Don’t capture unnecessary data
  3. Use closures wisely - Don’t overuse for simple cases
  4. Prefer const/let over var to avoid issues
  5. Document closure behavior - Help other developers
  6. Test edge cases - Especially with async code

Closures are fundamental to JavaScript. Understanding them deeply will make you a better developer and help you write more elegant, functional code.