What is a closure in JavaScript and how does it work?

👨‍💻 Frontend Developer 🟡 Often Asked 🎚️ Hard
#JavaScript #JS Basics

Brief Answer

Closure in JavaScript is a function that has access to variables from an outer scope even after the outer function has finished executing. A closure “remembers” the environment in which it was created.

function outerFunction(x) {
  // Outer variable
  return function innerFunction(y) {
    return x + y; // Access to x from outer scope
  };
}
 
const addFive = outerFunction(5);
console.log(addFive(3)); // 8

Complete Explanation of Closures

What is a Closure?

A closure is a combination of a function and the lexical environment in which that function was declared. This allows the function to access variables from the outer scope.

Key features of closures:

  • Function “remembers” variables from the outer scope
  • Access is preserved even after the outer function completes
  • Each closure has its own copy of variables
  • Variables remain “alive” while a reference to the closure exists

1. Basic Closure Examples

Simple Closure

function createGreeting(name) {
  const greeting = `Hello, ${name}!`;
  
  return function() {
    console.log(greeting); // Access to greeting from outer scope
  };
}
 
const greetAlex = createGreeting("Alexander");
const greetMaria = createGreeting("Maria");
 
greetAlex(); // "Hello, Alexander!"
greetMaria(); // "Hello, Maria!"

Closure with Parameters

function createMultiplier(multiplier) {
  return function(number) {
    return number * multiplier;
  };
}
 
const double = createMultiplier(2);
const triple = createMultiplier(3);
const square = createMultiplier(4);
 
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(square(5)); // 20

2. Practical Applications

Counters

function createCounter(initialValue = 0) {
  let count = initialValue;
  
  return {
    increment: () => ++count,
    decrement: () => --count,
    getValue: () => count,
    reset: () => count = initialValue
  };
}
 
const counter1 = createCounter();
const counter2 = createCounter(10);
 
console.log(counter1.increment()); // 1
console.log(counter1.increment()); // 2
console.log(counter2.increment()); // 11
 
console.log(counter1.getValue()); // 2
console.log(counter2.getValue()); // 11

Private Variables

function createBankAccount(initialBalance) {
  let balance = initialBalance;
  let transactionHistory = [];
  
  function addTransaction(type, amount) {
    transactionHistory.push({
      type,
      amount,
      date: new Date(),
      balance: balance
    });
  }
  
  return {
    deposit(amount) {
      if (amount > 0) {
        balance += amount;
        addTransaction('deposit', amount);
        return balance;
      }
      throw new Error('Amount must be positive');
    },
    
    withdraw(amount) {
      if (amount > 0 && amount <= balance) {
        balance -= amount;
        addTransaction('withdraw', amount);
        return balance;
      }
      throw new Error('Insufficient funds or invalid amount');
    },
    
    getBalance() {
      return balance;
    },
    
    getHistory() {
      return [...transactionHistory]; // Return a copy
    }
  };
}
 
const account = createBankAccount(1000);
console.log(account.deposit(500)); // 1500
console.log(account.withdraw(200)); // 1300
console.log(account.getBalance()); // 1300
 
// balance is not accessible directly!
// console.log(account.balance); // undefined

3. Closures in Loops

Classic Problem

// WRONG - all functions will output 3
for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // 3, 3, 3
  }, 100);
}

Solutions

// Solution 1: Using let
for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // 0, 1, 2
  }, 100);
}
 
// Solution 2: IIFE (Immediately Invoked Function Expression)
for (var i = 0; i < 3; i++) {
  (function(index) {
    setTimeout(function() {
      console.log(index); // 0, 1, 2
    }, 100);
  })(i);
}
 
// Solution 3: Factory function
function createLogger(value) {
  return function() {
    console.log(value);
  };
}
 
for (var i = 0; i < 3; i++) {
  setTimeout(createLogger(i), 100); // 0, 1, 2
}

4. Advanced Examples

Memoization

function memoize(fn) {
  const cache = new Map();
  
  return function(...args) {
    const key = JSON.stringify(args);
    
    if (cache.has(key)) {
      console.log('From cache:', key);
      return cache.get(key);
    }
    
    const result = fn.apply(this, args);
    cache.set(key, result);
    console.log('Computed:', key);
    return result;
  };
}
 
// Slow function for demonstration
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}
 
const memoizedFib = memoize(fibonacci);
 
console.log(memoizedFib(10)); // Computed: [10]
console.log(memoizedFib(10)); // From cache: [10]

Module Pattern

const Calculator = (function() {
  let history = [];
  let currentValue = 0;
  
  function addToHistory(operation, value, result) {
    history.push({
      operation,
      value,
      result,
      timestamp: Date.now()
    });
  }
  
  return {
    add(value) {
      const result = currentValue + value;
      addToHistory('add', value, result);
      currentValue = result;
      return this; // For method chaining
    },
    
    subtract(value) {
      const result = currentValue - value;
      addToHistory('subtract', value, result);
      currentValue = result;
      return this;
    },
    
    multiply(value) {
      const result = currentValue * value;
      addToHistory('multiply', value, result);
      currentValue = result;
      return this;
    },
    
    getValue() {
      return currentValue;
    },
    
    getHistory() {
      return [...history];
    },
    
    clear() {
      currentValue = 0;
      history = [];
      return this;
    }
  };
})();
 
// Usage
Calculator
  .add(10)
  .multiply(2)
  .subtract(5);
  
console.log(Calculator.getValue()); // 15
console.log(Calculator.getHistory());

Function with Configuration

function createApiClient(baseUrl, defaultHeaders = {}) {
  const headers = { ...defaultHeaders };
  
  function makeRequest(endpoint, options = {}) {
    const url = `${baseUrl}${endpoint}`;
    const requestHeaders = { ...headers, ...options.headers };
    
    return fetch(url, {
      ...options,
      headers: requestHeaders
    });
  }
  
  return {
    get(endpoint, options) {
      return makeRequest(endpoint, { ...options, method: 'GET' });
    },
    
    post(endpoint, data, options) {
      return makeRequest(endpoint, {
        ...options,
        method: 'POST',
        body: JSON.stringify(data),
        headers: {
          'Content-Type': 'application/json',
          ...options?.headers
        }
      });
    },
    
    setHeader(key, value) {
      headers[key] = value;
    },
    
    removeHeader(key) {
      delete headers[key];
    }
  };
}
 
const apiClient = createApiClient('https://api.example.com', {
  'Authorization': 'Bearer token123'
});
 
apiClient.setHeader('X-Custom-Header', 'value');
// apiClient.get('/users');
// apiClient.post('/users', { name: 'John' });

5. Closures and Performance

Memory Leaks

// BAD - can lead to memory leaks
function attachListeners() {
  const largeData = new Array(1000000).fill('data');
  
  document.getElementById('button').addEventListener('click', function() {
    // Closure holds reference to largeData
    console.log('Clicked!');
  });
}
 
// GOOD - avoid unnecessary references
function attachListeners() {
  const largeData = new Array(1000000).fill('data');
  
  function handleClick() {
    console.log('Clicked!');
  }
  
  document.getElementById('button').addEventListener('click', handleClick);
  
  // Clear reference if no longer needed
  // largeData = null; // If data is no longer needed
}

Optimization

// Create functions outside the loop
function createHandler(index) {
  return function() {
    console.log(`Handler ${index}`);
  };
}
 
// BAD - creating functions in loop
const handlers1 = [];
for (let i = 0; i < 1000; i++) {
  handlers1.push(function() {
    console.log(`Handler ${i}`);
  });
}
 
// BETTER - reuse factory function
const handlers2 = [];
for (let i = 0; i < 1000; i++) {
  handlers2.push(createHandler(i));
}

Practical Tasks

Task 1: What will the code output?

function createFunctions() {
  const functions = [];
  
  for (var i = 0; i < 3; i++) {
    functions.push(function() {
      return i;
    });
  }
  
  return functions;
}
 
const funcs = createFunctions();
console.log(funcs[0]()); // ?
console.log(funcs[1]()); // ?
console.log(funcs[2]()); // ?
Answer

3, 3, 3

All functions reference the same variable i, which equals 3 after the loop completes.

Fix:

for (let i = 0; i < 3; i++) { // let instead of var
  functions.push(function() {
    return i;
  });
}

Task 2: Create a timer function

// Create a function that returns an object with methods
// start(), stop(), getTime()
// Timer should count seconds
 
function createTimer() {
  // Your code here
}
 
const timer = createTimer();
timer.start();
// After 3 seconds
console.log(timer.getTime()); // ~3
timer.stop();
Answer
function createTimer() {
  let startTime = null;
  let elapsedTime = 0;
  let isRunning = false;
  let intervalId = null;
  
  return {
    start() {
      if (!isRunning) {
        startTime = Date.now() - elapsedTime;
        isRunning = true;
      }
    },
    
    stop() {
      if (isRunning) {
        elapsedTime = Date.now() - startTime;
        isRunning = false;
      }
    },
    
    getTime() {
      if (isRunning) {
        return Math.floor((Date.now() - startTime) / 1000);
      }
      return Math.floor(elapsedTime / 1000);
    },
    
    reset() {
      startTime = null;
      elapsedTime = 0;
      isRunning = false;
    }
  };
}

Task 3: Function with call limit

// Create a function that can be called only n times
function createLimitedFunction(fn, limit) {
  // Your code here
}
 
const limitedLog = createLimitedFunction(console.log, 3);
limitedLog('Call 1'); // Will output
limitedLog('Call 2'); // Will output
limitedLog('Call 3'); // Will output
limitedLog('Call 4'); // Won't output
Answer
function createLimitedFunction(fn, limit) {
  let callCount = 0;
  
  return function(...args) {
    if (callCount < limit) {
      callCount++;
      return fn.apply(this, args);
    }
    console.log(`Function can only be called ${limit} time(s)`);
  };
}

Task 4: Caching function

// Create a function that caches computation results
function createCachedFunction(fn, maxCacheSize = 10) {
  // Your code here
}
 
function expensiveOperation(n) {
  console.log(`Computing for ${n}`);
  return n * n;
}
 
const cached = createCachedFunction(expensiveOperation, 3);
console.log(cached(5)); // Computing for 5, returns 25
console.log(cached(5)); // From cache, returns 25
Answer
function createCachedFunction(fn, maxCacheSize = 10) {
  const cache = new Map();
  
  return function(...args) {
    const key = JSON.stringify(args);
    
    if (cache.has(key)) {
      return cache.get(key);
    }
    
    const result = fn.apply(this, args);
    
    // If cache is full, remove oldest element
    if (cache.size >= maxCacheSize) {
      const firstKey = cache.keys().next().value;
      cache.delete(firstKey);
    }
    
    cache.set(key, result);
    return result;
  };
}

Task 5: What will this code output?

function mystery() {
  let x = 1;
  
  function inner() {
    console.log(x);
    let x = 2;
  }
  
  inner();
}
 
mystery();
Answer

ReferenceError: Cannot access 'x' before initialization

Due to hoisting, the variable x is declared in the inner function, but access occurs before initialization (temporal dead zone).


Modern Features

Closures with async/await

function createAsyncCounter() {
  let count = 0;
  
  return {
    async increment(delay = 1000) {
      await new Promise(resolve => setTimeout(resolve, delay));
      return ++count;
    },
    
    async getCount() {
      return count;
    }
  };
}
 
const asyncCounter = createAsyncCounter();
 
// Usage
(async () => {
  console.log(await asyncCounter.increment()); // 1 (after 1 sec)
  console.log(await asyncCounter.increment(500)); // 2 (after 0.5 sec)
  console.log(await asyncCounter.getCount()); // 2
})();

Closures with Generators

function createSequenceGenerator(start = 0, step = 1) {
  let current = start;
  
  return function* () {
    while (true) {
      yield current;
      current += step;
    }
  };
}
 
const evenNumbers = createSequenceGenerator(0, 2)();
const oddNumbers = createSequenceGenerator(1, 2)();
 
console.log(evenNumbers.next().value); // 0
console.log(evenNumbers.next().value); // 2
console.log(oddNumbers.next().value); // 1
console.log(oddNumbers.next().value); // 3

Common Mistakes and Pitfalls

1. Unexpected Behavior in Loops

// ERROR: all handlers reference the last value
const buttons = document.querySelectorAll('button');
for (var i = 0; i < buttons.length; i++) {
  buttons[i].onclick = function() {
    alert('Button ' + i); // Always the last i
  };
}
 
// FIX: use let or closure
for (let i = 0; i < buttons.length; i++) {
  buttons[i].onclick = function() {
    alert('Button ' + i); // Correct i
  };
}

2. Memory Leaks

// PROBLEM: closure holds reference to large object
function createHandler(largeObject) {
  return function(event) {
    // Using only one property
    console.log(largeObject.id);
  };
}
 
// SOLUTION: extract only needed data
function createHandler(largeObject) {
  const id = largeObject.id; // Copy only what's needed
  return function(event) {
    console.log(id);
  };
}

3. Misunderstanding Scope

// What will this output?
function test() {
  console.log(a); // ?
  console.log(b); // ?
  
  var a = 1;
  let b = 2;
}
 
test();
// a: undefined (hoisting)
// b: ReferenceError (temporal dead zone)

Best Practices

1. Use Closures for Encapsulation

// Good: private data
function createUser(name, email) {
  // Private variables
  let _name = name;
  let _email = email;
  let _isActive = true;
  
  return {
    getName: () => _name,
    getEmail: () => _email,
    isActive: () => _isActive,
    deactivate: () => _isActive = false,
    // Validation on change
    setEmail: (newEmail) => {
      if (newEmail.includes('@')) {
        _email = newEmail;
      } else {
        throw new Error('Invalid email');
      }
    }
  };
}

2. Avoid Unnecessary Closures

// Bad: unnecessary closure
function processArray(arr) {
  return arr.map(function(item) {
    return item * 2;
  });
}
 
// Better: simple function
function double(item) {
  return item * 2;
}
 
function processArray(arr) {
  return arr.map(double);
}

3. Document Complex Closures

/**
 * Creates a function with call frequency limitation (throttle)
 * @param {Function} fn - Function to limit
 * @param {number} delay - Delay in milliseconds
 * @returns {Function} Limited function
 */
function createThrottledFunction(fn, delay) {
  let lastCallTime = 0;
  let timeoutId = null;
  
  return function(...args) {
    const now = Date.now();
    
    if (now - lastCallTime >= delay) {
      lastCallTime = now;
      fn.apply(this, args);
    } else if (!timeoutId) {
      timeoutId = setTimeout(() => {
        lastCallTime = Date.now();
        timeoutId = null;
        fn.apply(this, args);
      }, delay - (now - lastCallTime));
    }
  };
}

Summary

Closures are a powerful JavaScript mechanism that allows:

Creating private variables and methods
Preserving state between function calls
Implementing patterns like module and factory
Creating specialized functions (currying, memoization)
Managing variable scope

Important to remember:

  • Closures can lead to memory leaks
  • Variables in closures remain “alive”
  • Each function call creates a new closure
  • let and const behave differently in loops

Modern alternatives:

  • Classes for encapsulation
  • ES6 modules for code organization
  • WeakMap for private data

Closures are the foundation of understanding JavaScript. Master them well, and you’ll be able to write more elegant and functional code!


Want more interview preparation articles? Subscribe to EasyAdvice, bookmark the site and improve yourself every day 💪