Hack Frontend Community

Closures in JavaScript — замыкания JavaScript

What is a Closure?

A Closure is a function that has access to variables from its outer (parent) scope, even after that outer function has finished executing.

In Simple Terms

A closure allows a function to "remember" the environment in which it was created and use variables from that environment later.


Basic Example

function outer() {
  let counter = 0;  // Variable from outer function
  
  function inner() {
    counter++;  // Access to outer function variable
    console.log(counter);
  }
  
  return inner;
}

const increment = outer();
increment();  // 1
increment();  // 2
increment();  // 3

What happens?

  1. Function outer creates variable counter and function inner
  2. Function inner is returned and assigned to increment
  3. Although outer has finished executing, inner retains access to counter
  4. Each call to increment() uses the same variable counter

Key Point:

A closure is created automatically every time a function is created inside another function and has access to its variables.


How do Closures Work?

Closures work thanks to the Lexical Environment.

Scope Chain

let global = 'Global';

function outer() {
  let outerVar = 'Outer';
  
  function inner() {
    let innerVar = 'Inner';
    
    console.log(innerVar);   // Access to innerVar
    console.log(outerVar);   // Access to outerVar (closure)
    console.log(global);     // Access to global
  }
  
  return inner;
}

const closure = outer();
closure();

Practical Examples

Counter with Private Data

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.decrement());  // 1
console.log(counter.getCount());   // 1

// No direct access to count
console.log(counter.count);  // undefined

Encapsulation:

Closures allow creating private variables in JavaScript that cannot be changed directly from outside.

Function Factory

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

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

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

Each call to createMultiplier creates its own closure with its own multiplier value.

Event Listeners

function setupButtons() {
  const buttons = document.querySelectorAll('button');
  
  buttons.forEach((button, index) => {
    button.addEventListener('click', function() {
      console.log(`Button ${index} clicked`);  // Closure on index
    });
  });
}

Each event handler creates a closure that remembers its index.


Classic Loop Problem

Problem

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

Why? All three functions use the same closure with one variable i. By the time setTimeout executes, the loop is finished and i = 3.

Solution 1: Use let

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

let creates a new variable on each iteration of the loop.

Solution 2: IIFE (pre-ES6)

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

IIFE creates a new scope with its own copy of i.


Patterns with Closures

Module Pattern

const Calculator = (function() {
  // Private variables and functions
  let result = 0;
  
  function log(operation, value) {
    console.log(`${operation}: ${value}, result = ${result}`);
  }
  
  // Public API
  return {
    add: function(value) {
      result += value;
      log('Add', value);
      return this;
    },
    subtract: function(value) {
      result -= value;
      log('Subtract', value);
      return this;
    },
    getResult: function() {
      return result;
    }
  };
})();

Calculator
  .add(10)
  .add(5)
  .subtract(3);

console.log(Calculator.getResult());  // 12

Memoization

function memoize(fn) {
  const cache = {};  // Private cache
  
  return function(...args) {
    const key = JSON.stringify(args);
    
    if (key in cache) {
      console.log('From cache');
      return cache[key];
    }
    
    console.log('Computing');
    const result = fn(...args);
    cache[key] = result;
    return result;
  };
}

const expensiveOperation = memoize((n) => {
  let sum = 0;
  for (let i = 0; i < n; i++) {
    sum += i;
  }
  return sum;
});

console.log(expensiveOperation(1000000));  // Computing
console.log(expensiveOperation(1000000));  // From cache

Currying

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }
    
    return function(...nextArgs) {
      return curried.apply(this, args.concat(nextArgs));
    };
  };
}

const sum = (a, b, c) => a + b + c;
const curriedSum = curry(sum);

console.log(curriedSum(1)(2)(3));      // 6
console.log(curriedSum(1, 2)(3));      // 6
console.log(curriedSum(1)(2, 3));      // 6

React and Closures

"Stale Closure" Problem

function Counter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const timer = setInterval(() => {
      // Will always use initial value count = 0
      setCount(count + 1);
    }, 1000);
    
    return () => clearInterval(timer);
  }, []);  // Empty dependency array
  
  return <div>{count}</div>;
}

Solution

function Counter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const timer = setInterval(() => {
      // Uses current value
      setCount(prevCount => prevCount + 1);
    }, 1000);
    
    return () => clearInterval(timer);
  }, []);
  
  return <div>{count}</div>;
}

Memory Leaks

Closures can lead to memory leaks if they hold large objects.

Memory Leak

function createHeavyClosure() {
  const hugeArray = new Array(1000000).fill('data');
  
  return function() {
    console.log('Hello');
    // Function doesn't use hugeArray, but it stays in memory
  };
}

const fn = createHeavyClosure();

Solution

function createHeavyClosure() {
  const hugeArray = new Array(1000000).fill('data');
  
  // Use only what's needed
  const dataLength = hugeArray.length;
  
  return function() {
    console.log('Array length:', dataLength);
    // hugeArray can be garbage collected
  };
}

Frequently Asked Questions

What will this code output?

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

const funcs = createFunctions();
funcs[0]();  // ?
funcs[1]();  // ?
funcs[2]();  // ?

Answer: All three calls will output 3, because all functions close over the same variable i, which equals 3 after the loop.


Conclusion

Closures are:

  • Function + its lexical environment
  • Access to outer function variables after its completion
  • Foundation for modules, currying, memoization
  • Way to create private data
  • Potential source of memory leaks
  • Cause of "stale" values in React hooks

In Interviews:

Be prepared to:

  • Explain what a closure is in simple terms
  • Provide practical use case examples
  • Solve problems with loops and setTimeout
  • Explain memory leak issues
  • Show how closures work in React