Что такое замыкание в JavaScript и как оно работает?

👨‍💻 Frontend Developer 🟡 Часто попадается 🎚️ Неизвестно
#JavaScript #База JS

Краткий ответ

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

function outerFunction(x) {
  // Внешняя переменная
  return function innerFunction(y) {
    return x + y; // Доступ к x из внешней области
  };
}
 
const addFive = outerFunction(5);
console.log(addFive(3)); // 8

Полное объяснение замыканий

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

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

Ключевые особенности замыканий:

  • Функция “запоминает” переменные из внешней области
  • Доступ сохраняется даже после завершения внешней функции
  • Каждое замыкание имеет свою собственную копию переменных
  • Переменные остаются “живыми” пока существует ссылка на замыкание

1. Базовые примеры замыканий

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

function createGreeting(name) {
  const greeting = `Привет, ${name}!`;
  
  return function() {
    console.log(greeting); // Доступ к greeting из внешней области
  };
}
 
const greetAlex = createGreeting("Александр");
const greetMaria = createGreeting("Мария");
 
greetAlex(); // "Привет, Александр!"
greetMaria(); // "Привет, Мария!"

Замыкание с параметрами

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. Практические применения

Счетчики

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

Приватные переменные

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('Сумма должна быть положительной');
    },
    
    withdraw(amount) {
      if (amount > 0 && amount <= balance) {
        balance -= amount;
        addTransaction('withdraw', amount);
        return balance;
      }
      throw new Error('Недостаточно средств или неверная сумма');
    },
    
    getBalance() {
      return balance;
    },
    
    getHistory() {
      return [...transactionHistory]; // Возвращаем копию
    }
  };
}
 
const account = createBankAccount(1000);
console.log(account.deposit(500)); // 1500
console.log(account.withdraw(200)); // 1300
console.log(account.getBalance()); // 1300
 
// balance недоступен напрямую!
// console.log(account.balance); // undefined

3. Замыкания в циклах

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

// НЕПРАВИЛЬНО - все функции выведут 3
for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // 3, 3, 3
  }, 100);
}

Решения

// Решение 1: Использование let
for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // 0, 1, 2
  }, 100);
}
 
// Решение 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);
}
 
// Решение 3: Функция-фабрика
function createLogger(value) {
  return function() {
    console.log(value);
  };
}
 
for (var i = 0; i < 3; i++) {
  setTimeout(createLogger(i), 100); // 0, 1, 2
}

4. Продвинутые примеры

Мемоизация

function memoize(fn) {
  const cache = new Map();
  
  return function(...args) {
    const key = JSON.stringify(args);
    
    if (cache.has(key)) {
      console.log('Из кеша:', key);
      return cache.get(key);
    }
    
    const result = fn.apply(this, args);
    cache.set(key, result);
    console.log('Вычислено:', key);
    return result;
  };
}
 
// Медленная функция для демонстрации
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}
 
const memoizedFib = memoize(fibonacci);
 
console.log(memoizedFib(10)); // Вычислено: [10]
console.log(memoizedFib(10)); // Из кеша: [10]

Модуль-паттерн

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; // Для цепочки вызовов
    },
    
    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;
    }
  };
})();
 
// Использование
Calculator
  .add(10)
  .multiply(2)
  .subtract(5);
  
console.log(Calculator.getValue()); // 15
console.log(Calculator.getHistory());

Функция с настройками

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. Замыкания и производительность

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

// ПЛОХО - может привести к утечке памяти
function attachListeners() {
  const largeData = new Array(1000000).fill('data');
  
  document.getElementById('button').addEventListener('click', function() {
    // Замыкание держит ссылку на largeData
    console.log('Clicked!');
  });
}
 
// ХОРОШО - избегаем ненужных ссылок
function attachListeners() {
  const largeData = new Array(1000000).fill('data');
  
  function handleClick() {
    console.log('Clicked!');
  }
  
  document.getElementById('button').addEventListener('click', handleClick);
  
  // Очищаем ссылку, если она больше не нужна
  // largeData = null; // Если данные больше не нужны
}

Оптимизация

// Создание функций вне цикла
function createHandler(index) {
  return function() {
    console.log(`Handler ${index}`);
  };
}
 
// ПЛОХО - создаем функции в цикле
const handlers1 = [];
for (let i = 0; i < 1000; i++) {
  handlers1.push(function() {
    console.log(`Handler ${i}`);
  });
}
 
// ЛУЧШЕ - переиспользуем функцию-фабрику
const handlers2 = [];
for (let i = 0; i < 1000; i++) {
  handlers2.push(createHandler(i));
}

Практические задачи

Задача 1: Что выведет код?

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]()); // ?
Ответ

3, 3, 3

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

Исправление:

for (let i = 0; i < 3; i++) { // let вместо var
  functions.push(function() {
    return i;
  });
}

Задача 2: Создайте функцию-таймер

// Создайте функцию, которая возвращает объект с методами
// start(), stop(), getTime()
// Таймер должен считать секунды
 
function createTimer() {
  // Ваш код здесь
}
 
const timer = createTimer();
timer.start();
// Через 3 секунды
console.log(timer.getTime()); // ~3
timer.stop();
Ответ
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;
    }
  };
}

Задача 3: Функция с ограничением вызовов

// Создайте функцию, которая может быть вызвана только n раз
function createLimitedFunction(fn, limit) {
  // Ваш код здесь
}
 
const limitedLog = createLimitedFunction(console.log, 3);
limitedLog('Вызов 1'); // Выведет
limitedLog('Вызов 2'); // Выведет
limitedLog('Вызов 3'); // Выведет
limitedLog('Вызов 4'); // Не выведет
Ответ
function createLimitedFunction(fn, limit) {
  let callCount = 0;
  
  return function(...args) {
    if (callCount < limit) {
      callCount++;
      return fn.apply(this, args);
    }
    console.log(`Функция может быть вызвана только ${limit} раз(а)`);
  };
}

Задача 4: Кеширующая функция

// Создайте функцию, которая кеширует результаты вычислений
function createCachedFunction(fn, maxCacheSize = 10) {
  // Ваш код здесь
}
 
function expensiveOperation(n) {
  console.log(`Вычисляем для ${n}`);
  return n * n;
}
 
const cached = createCachedFunction(expensiveOperation, 3);
console.log(cached(5)); // Вычисляем для 5, возвращает 25
console.log(cached(5)); // Из кеша, возвращает 25
Ответ
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.size >= maxCacheSize) {
      const firstKey = cache.keys().next().value;
      cache.delete(firstKey);
    }
    
    cache.set(key, result);
    return result;
  };
}

Задача 5: Что выведет этот код?

function mystery() {
  let x = 1;
  
  function inner() {
    console.log(x);
    let x = 2;
  }
  
  inner();
}
 
mystery();
Ответ

ReferenceError: Cannot access 'x' before initialization

Из-за hoisting переменная x объявлена в функции inner, но обращение к ней происходит до инициализации (temporal dead zone).


Современные возможности

Замыкания с 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();
 
// Использование
(async () => {
  console.log(await asyncCounter.increment()); // 1 (через 1 сек)
  console.log(await asyncCounter.increment(500)); // 2 (через 0.5 сек)
  console.log(await asyncCounter.getCount()); // 2
})();

Замыкания с генераторами

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

Частые ошибки и подводные камни

1. Неожиданное поведение в циклах

// ОШИБКА: все обработчики ссылаются на последнее значение
const buttons = document.querySelectorAll('button');
for (var i = 0; i < buttons.length; i++) {
  buttons[i].onclick = function() {
    alert('Кнопка ' + i); // Всегда последний i
  };
}
 
// ИСПРАВЛЕНИЕ: используем let или замыкание
for (let i = 0; i < buttons.length; i++) {
  buttons[i].onclick = function() {
    alert('Кнопка ' + i); // Правильный i
  };
}

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

// ПРОБЛЕМА: замыкание держит ссылку на большой объект
function createHandler(largeObject) {
  return function(event) {
    // Используем только одно свойство
    console.log(largeObject.id);
  };
}
 
// РЕШЕНИЕ: извлекаем только нужные данные
function createHandler(largeObject) {
  const id = largeObject.id; // Копируем только нужное
  return function(event) {
    console.log(id);
  };
}

3. Неправильное понимание области видимости

// Что выведет?
function test() {
  console.log(a); // ?
  console.log(b); // ?
  
  var a = 1;
  let b = 2;
}
 
test();
// a: undefined (hoisting)
// b: ReferenceError (temporal dead zone)

Лучшие практики

1. Используйте замыкания для инкапсуляции

// Хорошо: приватные данные
function createUser(name, email) {
  // Приватные переменные
  let _name = name;
  let _email = email;
  let _isActive = true;
  
  return {
    getName: () => _name,
    getEmail: () => _email,
    isActive: () => _isActive,
    deactivate: () => _isActive = false,
    // Валидация при изменении
    setEmail: (newEmail) => {
      if (newEmail.includes('@')) {
        _email = newEmail;
      } else {
        throw new Error('Неверный email');
      }
    }
  };
}

2. Избегайте ненужных замыканий

// Плохо: ненужное замыкание
function processArray(arr) {
  return arr.map(function(item) {
    return item * 2;
  });
}
 
// Лучше: простая функция
function double(item) {
  return item * 2;
}
 
function processArray(arr) {
  return arr.map(double);
}

3. Документируйте сложные замыкания

/**
 * Создает функцию с ограничением частоты вызовов (throttle)
 * @param {Function} fn - Функция для ограничения
 * @param {number} delay - Задержка в миллисекундах
 * @returns {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));
    }
  };
}

Резюме

Замыкания — это мощный механизм JavaScript, который позволяет:

Создавать приватные переменные и методы
Сохранять состояние между вызовами функций
Реализовывать паттерны вроде модуля и фабрики
Создавать специализированные функции (каррирование, мемоизация)
Управлять областью видимости переменных

Важно помнить:

  • Замыкания могут приводить к утечкам памяти
  • Переменные в замыкании остаются “живыми”
  • Каждый вызов функции создает новое замыкание
  • let и const ведут себя по-разному в циклах

Современные альтернативы:

  • Классы для инкапсуляции
  • Модули ES6 для организации кода
  • WeakMap для приватных данных

Замыкания — это основа понимания JavaScript. Изучите их хорошо, и вы сможете писать более элегантный и функциональный код!


Хочешь больше статей по подготовке к собеседованию? Подпишись на EasyAdvice(@AleksandrEmolov_EasyAdvice), добавляй сайт в избранное и прокачивай себя каждый день 💪