Замыкания в JavaScript простыми словами: как они работают и зачем нужны

Замыкания в JavaScript простыми словами: как они работают и зачем нужны

Замыкания в JavaScript простыми словами: как они работают и зачем нужны

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

Что такое лексическое окружение

Когда JavaScript выполняет функцию, он создает лексическое окружение с локальными переменными. Вложенная функция получает доступ к этим переменным и «замыкает» их на себя. После возврата внешней функции вложенная всё ещё может читать и менять те значения, пока существует хотя бы одна ссылка на вложенную функцию.

Базовый пример: счётчик

function createCounter(start) {
  var count = start || 0;
  return function() {
    count += 1;
    return count;
  };
}

var counter = createCounter(5);
console.log(counter()); // 6
console.log(counter()); // 7
console.log(counter()); // 8

Функция, которую вернула createCounter, замкнула переменную count. Она будет жить, пока жива ссылка на counter.

Практика: приватные данные и инкапсуляция

Замыкания — простой способ скрыть состояние без классов и модификаторов доступа.

function createWallet(initial) {
  var balance = initial || 0;
  return {
    deposit: function(amount) {
      if (amount <= 0) return false;
      balance += amount;
      return true;
    },
    withdraw: function(amount) {
      if (amount <= 0 || amount > balance) return false;
      balance -= amount;
      return true;
    },
    getBalance: function() {
      return balance;
    }
  };
}

var wallet = createWallet(100);
wallet.deposit(50);
console.log(wallet.getBalance()); // 150
console.log(wallet.balance); // undefined — приватно!

Частичное применение и конфигурирование функций

Замыкания удобны для создания «преднастроенных» функций с зафиксированными параметрами.

function makeFormatter(prefix, suffix) {
  return function(value) {
    return prefix + String(value) + suffix;
  };
}

var asCurrency = makeFormatter('$', ' USD');
console.log(asCurrency(10)); // "$10 USD"

Memoization: ускоряем дорогие вычисления

Замыкание может хранить кэш результатов, чтобы не пересчитывать одно и то же.

function memoize(fn) {
  var cache = {};
  return function(key) {
    if (cache.hasOwnProperty(key)) {
      return cache[key];
    }
    var result = fn(key);
    cache[key] = result;
    return result;
  };
}

function heavy(x) {
  // некий дорогой расчёт
  for (var i = 0; i < 1e7; i++) {}
  return x * x;
}

var fastHeavy = memoize(heavy);
console.log(fastHeavy(9));  // считается впервые
console.log(fastHeavy(9));  // мгновенно из кэша

Типичные ошибки: замыкания в циклах и var

Классическая ловушка — использовать var в цикле с асинхронщиной. Все колбэки видят одну и ту же переменную:

for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log('var i =', i); // 3, 3, 3
  }, 0);
}

Решения:

  • Использовать let (блочная область видимости).
  • Или немедленно вызываемую функцию (IIFE), чтобы «зафиксировать» текущее значение.
  • // 1) let
    for (let i = 0; i < 3; i++) {
      setTimeout(function() {
        console.log('let i =', i); // 0, 1, 2
      }, 0);
    }
    
    // 2) IIFE c var
    for (var j = 0; j < 3; j++) {
      (function(jCopy) {
        setTimeout(function() {
          console.log('IIFE j =', jCopy); // 0, 1, 2
        }, 0);
      })(j);
    }
    

    Замыкания и обработчики событий

    Часто нужно «помнить» данные при навешивании слушателей.

    function registerHandlers(buttons) {
      for (var i = 0; i < buttons.length; i++) {
        (function(index) {
          buttons[index].addEventListener('click', function() {
            console.log('Клик по кнопке #' + index);
          });
        })(i);
      }
    }
    

    С let код проще и безопаснее:

    function registerHandlersModern(buttons) {
      for (let i = 0; i < buttons.length; i++) {
        buttons[i].addEventListener('click', function() {
          console.log('Клик по кнопке #' + i);
        });
      }
    }
    

    Перформанс и утечки памяти

  • Не держите лишние ссылки на функции-замыкания, если они уже не нужны — так вы удерживаете всё их окружение.
  • Отписывайтесь от обработчиков событий, если DOM-элемент удалён, иначе замыкания будут удерживать память.
  • Для горячих путей избегайте чрезмерной вложенности функций — профилируйте и упрощайте.
  • Как понять, что у вас замыкание?

    Признаки:

  • Вложенная функция использует переменные не из своих параметров и не из глобальной области, а из внешней функции.
  • Эта внешняя функция уже завершилась, но значения продолжают «жить».
  • Чек-лист по применению замыканий

  • Инкапсуляция состояния (счётчики, кошельки, настройки).
  • Создание фабрик функций и частичное применение аргументов.
  • Кэширование (memoization), debounce/throttle-обёртки.
  • Навешивание обработчиков с доступом к нужному контексту данных.
  • Мини-практика: debounce на замыканиях

    Debounce откладывает вызов функции, пока пользователь продолжает ввод. Замыкание хранит таймер.

    function debounce(fn, delay) {
      var timer = null;
      return function() {
        var ctx = this;
        var args = arguments;
        clearTimeout(timer);
        timer = setTimeout(function() {
          fn.apply(ctx, args);
        }, delay);
      };
    }
    
    var onInput = debounce(function(e) {
      console.log('Запрос по:', e.target.value);
    }, 300);
    

    Итоги

    Мы разобрали замыкания в JavaScript простыми словами, увидели их пользу в реальном коде, обсудили типичные ошибки и как их избегать. Отработайте примеры, поэкспериментируйте с var, let и асинхронностью — так понимание закрепится.

    Хотите системно прокачать основы и уверенно писать на JavaScript? Рекомендую пройти практический курс «JavaScript: с Нуля до Гуру 2.0» — пошаговое обучение с реальными проектами.

    Источник

    НЕТ КОММЕНТАРИЕВ

    Оставить комментарий