javascript-today

Closures

Closures

Closures are one of JavaScript’s most powerful features, yet they can be tricky to understand at first. A closure gives you access to an outer function’s scope from an inner function, even after the outer function has finished executing.

The Simple Explanation

A closure is created when:

  1. A function is defined inside another function
  2. The inner function uses variables from the outer function
  3. The inner function is returned or passed elsewhere

The inner function “closes over” (remembers) the outer function’s variables.

Basic Example

function createGreeter(greeting) {
  // 'greeting' is a variable in the outer function
  
  return function(name) {
    // This inner function has access to 'greeting'
    console.log(greeting + ", " + name);
  };
}

const sayHello = createGreeter("Hello");
const sayHi = createGreeter("Hi");

sayHello("Alice");  // "Hello, Alice"
sayHi("Bob");       // "Hi, Bob"

Even though createGreeter finished executing, the returned function still remembers the greeting variable. That’s a closure!

Why Closures Matter

1. Data Privacy

Create private variables that can’t be accessed directly:

function createCounter() {
  let count = 0;  // Private variable
  
  return {
    increment: function() {
      count++;
      return count;
    },
    decrement: function() {
      count--;
      return count;
    },
    getCount: function() {
      return count;
    }
  };
}

const counter = createCounter();
console.log(counter.increment());  // 1
console.log(counter.increment());  // 2
console.log(counter.getCount());   // 2
console.log(counter.count);        // undefined - can't access directly!

2. Function Factories

Create specialized functions:

function multiplyBy(factor) {
  return function(number) {
    return number * factor;
  };
}

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

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

3. Event Handlers with Context

function createButtonHandler(buttonId) {
  return function() {
    console.log("Button " + buttonId + " was clicked!");
  };
}

// Each button remembers its own ID
const button1Handler = createButtonHandler(1);
const button2Handler = createButtonHandler(2);

button1Handler();  // "Button 1 was clicked!"
button2Handler();  // "Button 2 was clicked!"

Real-World Examples

Example 1: User Session Management

function createUserSession(username) {
  let loginTime = new Date();
  let isActive = true;
  
  return {
    getUsername: function() {
      return username;
    },
    getLoginTime: function() {
      return loginTime;
    },
    logout: function() {
      isActive = false;
      console.log(username + " logged out");
    },
    isActive: function() {
      return isActive;
    }
  };
}

const session = createUserSession("alice123");
console.log(session.getUsername());  // "alice123"
console.log(session.isActive());     // true
session.logout();                    // "alice123 logged out"
console.log(session.isActive());     // false

Example 2: Once Function (run only once)

function once(fn) {
  let hasRun = false;
  let result;
  
  return function(...args) {
    if (!hasRun) {
      result = fn(...args);
      hasRun = true;
    }
    return result;
  };
}

const initializeApp = once(() => {
  console.log("App initialized!");
  return "initialized";
});

initializeApp();  // "App initialized!"
initializeApp();  // Nothing logged - already ran
initializeApp();  // Still nothing

Example 3: Debounce Function

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

// Search function that waits for user to stop typing
const searchAPI = debounce((query) => {
  console.log("Searching for:", query);
}, 500);

searchAPI("j");       // Waits...
searchAPI("ja");      // Waits...
searchAPI("jav");     // Waits...
searchAPI("java");    // After 500ms: "Searching for: java"

Common Pitfall: Closures in Loops

This is a classic mistake:

// Problem: All functions reference the same 'i'
for (var i = 1; i <= 3; i++) {
  setTimeout(function() {
    console.log(i);  // What will this print?
  }, i * 1000);
}
// Output: 4, 4, 4 (not 1, 2, 3!)

Why? By the time the functions run, the loop has finished and i is 4.

Solution 1: Use let instead of var

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

Solution 2: Create a closure with an IIFE

for (var i = 1; i <= 3; i++) {
  (function(index) {
    setTimeout(function() {
      console.log(index);
    }, index * 1000);
  })(i);
}
// Output: 1, 2, 3 ✓

Module Pattern

Closures enable the popular module pattern:

const ShoppingCart = (function() {
  // Private variables and functions
  let items = [];
  
  function calculateTotal() {
    return items.reduce((sum, item) => sum + item.price, 0);
  }
  
  // Public API
  return {
    addItem: function(item) {
      items.push(item);
    },
    
    removeItem: function(itemId) {
      items = items.filter(item => item.id !== itemId);
    },
    
    getTotal: function() {
      return calculateTotal();
    },
    
    getItemCount: function() {
      return items.length;
    }
  };
})();

ShoppingCart.addItem({ id: 1, name: "Book", price: 10 });
ShoppingCart.addItem({ id: 2, name: "Pen", price: 2 });
console.log(ShoppingCart.getTotal());       // 12
console.log(ShoppingCart.getItemCount());   // 2
console.log(ShoppingCart.items);            // undefined - private!

Memory Considerations

Closures keep variables in memory. Usually not a problem, but be aware:

function createHugeArray() {
  const hugeArray = new Array(1000000).fill("data");
  
  return function() {
    // This closure keeps hugeArray in memory
    return hugeArray[0];
  };
}

// hugeArray stays in memory as long as the function exists
const getter = createHugeArray();

If you’re done with the closure, set it to null:

let getter = createHugeArray();
// ... use it ...
getter = null;  // Now hugeArray can be garbage collected

Arrow Functions and Closures

Arrow functions work great with closures:

const createMultiplier = (factor) => {
  return (number) => number * factor;
};

// Or even shorter
const createMultiplier = factor => number => number * factor;

const times5 = createMultiplier(5);
console.log(times5(3));  // 15

Key Takeaways

  1. Closures happen automatically when inner functions access outer variables
  2. They remember variables even after the outer function returns
  3. Use for: Private data, function factories, event handlers, modules
  4. Be careful: With loops using var, and memory with large closures
  5. They’re everywhere: In callbacks, event handlers, React hooks, and more

Practice

Try to understand what this code does:

function createGame() {
  let score = 0;
  let level = 1;
  
  return {
    earnPoints: (points) => {
      score += points;
      if (score >= level * 100) {
        level++;
        console.log("Level up! Now level " + level);
      }
    },
    
    getStatus: () => ({
      score: score,
      level: level
    })
  };
}

const game = createGame();
game.earnPoints(50);
game.earnPoints(60);  // "Level up! Now level 2"
console.log(game.getStatus());  // { score: 110, level: 2 }