Hack Frontend Community

Замыкания (Closures) в JavaScript — замыкания JavaScript

Что такое замыкание?

Замыкание (Closure) — это функция, которая имеет доступ к переменным из своей внешней (родительской) области видимости, даже после того, как эта внешняя функция завершила выполнение.

Простыми словами

Замыкание позволяет функции «запомнить» окружение, в котором она была создана, и использовать переменные из этого окружения позже.


Базовый пример

function outer() {
  let counter = 0;  // Переменная внешней функции
  
  function inner() {
    counter++;  // Доступ к переменной внешней функции
    console.log(counter);
  }
  
  return inner;
}

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

Что происходит?

  1. Функция outer создаёт переменную counter и функцию inner
  2. Функция inner возвращается и присваивается в increment
  3. Хотя outer завершила выполнение, inner сохраняет доступ к counter
  4. При каждом вызове increment() используется одна и та же переменная counter

Ключевой момент:

Замыкание создаётся автоматически каждый раз, когда функция создаётся внутри другой функции и имеет доступ к её переменным.


Как работают замыкания?

Замыкания работают благодаря лексическому окружению (Lexical Environment).

Цепочка областей видимости

let global = 'Global';

function outer() {
  let outerVar = 'Outer';
  
  function inner() {
    let innerVar = 'Inner';
    
    console.log(innerVar);   // Доступ к innerVar
    console.log(outerVar);   // Доступ к outerVar (замыкание)
    console.log(global);     // Доступ к global
  }
  
  return inner;
}

const closure = outer();
closure();

Практические примеры

Счётчик с приватными данными

function createCounter() {
  let count = 0;  // Приватная переменная
  
  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

// Нет прямого доступа к count
console.log(counter.count);  // undefined

Инкапсуляция:

Замыкания позволяют создавать приватные переменные в JavaScript, которые нельзя изменить напрямую снаружи.

Функция-фабрика

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

Каждый вызов createMultiplier создаёт своё собственное замыкание со своим значением multiplier.

Event listeners

function setupButtons() {
  const buttons = document.querySelectorAll('button');
  
  buttons.forEach((button, index) => {
    button.addEventListener('click', function() {
      console.log(`Кнопка ${index} нажата`);  // Замыкание на index
    });
  });
}

Каждый обработчик события создаёт замыкание, которое запоминает свой index.


Классическая проблема с циклами

Проблема

for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}
// Выведет: 3, 3, 3 (а не 0, 1, 2)

Почему? Все три функции используют одно и то же замыкание с одной переменной i. К моменту выполнения setTimeout, цикл завершён и i = 3.

Решение 1: Использовать let

for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}
// Выведет: 0, 1, 2

let создаёт новую переменную на каждой итерации цикла.

Решение 2: IIFE (до ES6)

for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(function() {
      console.log(j);
    }, 1000);
  })(i);
}
// Выведет: 0, 1, 2

IIFE создаёт новую область видимости с собственной копией i.


Паттерны с замыканиями

Модуль (Module Pattern)

const Calculator = (function() {
  // Приватные переменные и функции
  let result = 0;
  
  function log(operation, value) {
    console.log(`${operation}: ${value}, result = ${result}`);
  }
  
  // Публичный 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 = {};  // Приватный кэш
  
  return function(...args) {
    const key = JSON.stringify(args);
    
    if (key in cache) {
      console.log('Из кэша');
      return cache[key];
    }
    
    console.log('Вычисляем');
    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));  // Вычисляем
console.log(expensiveOperation(1000000));  // Из кэша

Каррирование (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 и замыкания

Проблема "устаревшего замыкания"

function Counter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const timer = setInterval(() => {
      // Всегда будет использовать начальное значение count = 0
      setCount(count + 1);
    }, 1000);
    
    return () => clearInterval(timer);
  }, []);  // Пустой массив зависимостей
  
  return <div>{count}</div>;
}

Решение

function Counter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const timer = setInterval(() => {
      // Использует актуальное значение
      setCount(prevCount => prevCount + 1);
    }, 1000);
    
    return () => clearInterval(timer);
  }, []);
  
  return <div>{count}</div>;
}

Утечки памяти

Замыкания могут привести к утечкам памяти, если они удерживают большие объекты.

Утечка памяти

function createHeavyClosure() {
  const hugeArray = new Array(1000000).fill('data');
  
  return function() {
    console.log('Hello');
    // Функция не использует hugeArray, но он остаётся в памяти
  };
}

const fn = createHeavyClosure();

Решение

function createHeavyClosure() {
  const hugeArray = new Array(1000000).fill('data');
  
  // Используем только то, что нужно
  const dataLength = hugeArray.length;
  
  return function() {
    console.log('Array length:', dataLength);
    // hugeArray может быть удалён сборщиком мусора
  };
}

Часто задаваемые вопросы

Что выведет этот код?

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]();  // ?

Ответ: Все три вызова выведут 3, потому что все функции замыкаются на одной переменной i, которая после цикла равна 3.


Вывод

Замыкания — это:

  • Функция + её лексическое окружение
  • Доступ к переменным внешней функции после её завершения
  • Основа для модулей, каррирования, мемоизации
  • Способ создания приватных данных
  • Потенциальный источник утечек памяти
  • Причина "устаревших" значений в React хуках

На собеседовании:

Будьте готовы:

  • Объяснить, что такое замыкание простыми словами
  • Привести практические примеры использования
  • Решить задачи с циклами и setTimeout
  • Объяснить проблему утечек памяти
  • Показать, как работают замыкания в React

Связанные статьи