Skip to content
GeeksSmith
beginner 6 min read

Closures

A closure is a function bundled together with the lexical scope it was created in, so it keeps access to those outer variables even after the outer function has returned.

Why it matters in interviews

Closures are how JavaScript gives you private state without classes: counters, memoized caches, event handlers with captured context, and module patterns all rely on a function remembering variables from where it was defined, not from where it's called.

Visual & Interactive Explanation

Interactive Flow Execution

Code Examples & Implementation

Counter via closure
function createCounter() {
  let count = 0;
  return {
    increment: () => ++count,
    get value() {
      return count;
    },
  };
}

const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.value); // 2 — "count" is private, only reachable via the closure
The classic loop pitfall
// var is function-scoped: all three callbacks share ONE binding
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0); // 3, 3, 3
}

// let is block-scoped: each iteration gets its own binding
for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j), 0); // 0, 1, 2
}

Interview-Ready Answers

A function that remembers the variables from its outer scope, even after that outer function has finished running.

Official Documentation & Specifications

Follow-up Questions & Deep Dives

🎯 Code Output — What Does This Print?

Question 1medium
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
Question 2medium
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
Question 3hard
function createCounter() {
  let count = 0;
  return {
    increment: () => ++count,
    getCount: () => count,
  };
}
const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getCount());
console.log(counter.increment());
Question 4tricky
function outer() {
  var x = 10;
  function inner() {
    console.log(x);
    var x = 20;
  }
  inner();
}
outer();
Question 5hard
const funcs = [];
for (var i = 0; i < 3; i++) {
  funcs.push(
    (function(j) {
      return () => console.log(j);
    })(i)
  );
}
funcs[0]();
funcs[1]();
funcs[2]();

Common Mistakes & Anti-Patterns

  • Using var inside loops that create callbacks, expecting per-iteration values
  • Assuming closures deep-copy the variables they capture
  • Not clearing intervals/listeners that hold closures over large data

Implementation Evolution: Anti-Pattern to Production

❌ Anti-Pattern: Global State Pollution & Stale Callback Capture

// ❌ Broken: Global variable shared across all handlers, leading to race conditions
let activeTimer;

function debounce(fn, delay) {
  return function(...args) {
    clearTimeout(activeTimer);
    activeTimer = setTimeout(() => fn(...args), delay);
  };
}
Why this breaks: Using an outer module-level variable means multiple debounced functions overwrite each other's timers. Invoking debounce on search and scroll simultaneously cancels whichever timer was scheduled first.

✅ Correct Pattern: Encapsulated Closure Binding

// ✅ Correct: Private timer instance per debounced function instance
function debounce(fn, delay) {
  let timerId;
  return function(...args) {
    clearTimeout(timerId);
    timerId = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}
🚀

🚀 Production-Grade (Hardened): Production-Grade Closure with Cancel & Flush Methods

// 🚀 Production: TypeScript typed, preserves 'this', exposes .cancel() & .flush()
export function debounce<T extends (...args: any[]) => any>(
  fn: T,
  wait: number,
  options: { leading?: boolean } = {}
) {
  let timerId: ReturnType<typeof setTimeout> | undefined;
  let lastArgs: Parameters<T> | undefined;
  let lastThis: any;

  function debounced(this: any, ...args: Parameters<T>) {
    lastArgs = args;
    lastThis = this;

    const callNow = options.leading && !timerId;

    clearTimeout(timerId);
    timerId = setTimeout(() => {
      timerId = undefined;
      if (!options.leading && lastArgs) {
        fn.apply(lastThis, lastArgs);
      }
    }, wait);

    if (callNow) {
      fn.apply(lastThis, lastArgs);
    }
  }

  debounced.cancel = () => {
    clearTimeout(timerId);
    timerId = undefined;
    lastArgs = undefined;
  };

  debounced.flush = () => {
    if (timerId && lastArgs) {
      fn.apply(lastThis, lastArgs);
      debounced.cancel();
    }
  };

  return debounced;
}

⚠️ Trick Questions & Interviewer Traps

Q1:Do closures in JavaScript capture variables by value or by reference?

Q2:Can two separate inner functions share the exact same closure environment?

📋 Rapid Revision Cheat Sheet

  • Closure = Function + Lexical Scope reference (not a snapshot copy).
  • Live bindings: Modifying captured variables reflects in all sibling closures.
  • Loop trap: 'var' shares 1 function-scoped binding; 'let' creates N per-iteration bindings.
  • Memory leak trigger: Dangling listeners/timers holding closures to large objects.
  • V8 internals: Escaping variables migrate from Stack to Heap Context.

Real-World Architectural Scenario

You need a debounce utility that remembers the last timer across calls. How does a closure make this possible without a class?

Rate Your Readiness

Rate how comfortably you can explain this in an interview to update your global readiness gauge:

Rate your confidence: