На главную

Понимание замыканий в JavaScript для новичков через практику


Предисловие

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

Простой пример

Рассмотрим базовый пример, чтобы понять, как работает замыкание:

function outerFunction() {
  const message = 'Привет, мир!';
  function innerFunction() {
    console.log(message);
  }
  return innerFunction;
}

const myFunction = outerFunction();
myFunction(); // Выводит: Привет, мир!

Здесь innerFunction — это замыкание, которое имеет доступ к переменной message из внешней функции outerFunction. Даже после того, как outerFunction завершила выполнение, innerFunction продолжает “помнить” значение message.

Зачем нужны замыкания?

Замыкания полезны для:

  • Приватности данных: Создание переменных, недоступных извне.
  • Асинхронного программирования: Работа с таймерами и обработчиками событий.
  • Функциональных паттернов: Реализация каррирования и мемоизации.

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

Приветствие

Я часто вижу, как новички сталкиваются с трудностями в понимании замыканий в JavaScript. Однако замыкания — это одна из ключевых концепций языка, которая открывает множество возможностей для написания эффективного и гибкого кода. В этой статье я проведу тебя через мир замыканий, начиная с простых основ и постепенно переходя к более сложным примерам. Моя цель — помочь тебе не только понять, что такое замыкания, но и научиться использовать их в своих проектах, а также уверенно отвечать на вопросы о замыканиях на собеседованиях. Давай начнем!

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

Согласно MDN Web Docs, замыкание — это комбинация функции и её лексического окружения, которая позволяет функции сохранять доступ к переменным из внешней области видимости. Другими словами, замыкание — это функция, которая “помнит” переменные из того контекста, в котором она была создана, даже если этот контекст уже не существует.

Рассмотрим простой пример:

function makeFunc() {
  const name = "Mozilla";

  function displayName() {
    console.log(name);
  }

  return displayName;
}

const myFunc = makeFunc();
myFunc(); // Выводит: Mozilla

В этом коде:

  • makeFunc создает локальную переменную name и внутреннюю функцию displayName.
  • displayName — это замыкание, которое имеет доступ к name.
  • Когда мы вызываем makeFunc, она возвращает displayName, и даже после завершения makeFunc функция displayName продолжает “помнить” значение name.

Этот пример демонстрирует суть замыканий: они позволяют внутренней функции сохранять связь с внешними переменными.

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

Чтобы понять замыкания, нужно разобраться с лексической областью видимости. Лексическая область видимости означает, что область видимости переменной определяется её местоположением в исходном коде. В JavaScript функции создают свою собственную область видимости, а внутренние функции имеют доступ к переменным внешних функций.

Когда функция определяется, она “захватывает” переменные из своей внешней области видимости. Этот захват и формирует замыкание. Рассмотрим ещё один пример:

function outer() {
  const x = 10;

  function inner() {
    console.log(x);
  }

  return inner;
}

const closureFunc = outer();
closureFunc(); // Выводит: 10

Здесь inner захватывает переменную x из outer. Даже после завершения outer, inner сохраняет доступ к x.

Важно понимать, что замыкания захватывают переменные, а не их значения. Это означает, что если значение переменной изменяется, замыкание увидит актуальное значение. Например:

function numberGenerator() {
  let num = 1;

  function checkNumber() {
    console.log(num);
  }

  num++;
  return checkNumber;
}

const number = numberGenerator();
number(); // Выводит: 2

В этом примере num увеличивается до 2 перед возвратом checkNumber, поэтому замыкание выводит 2.

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

Замыкания — мощный инструмент, который используется во многих сценариях. Рассмотрим несколько практических примеров.

Приватность данных

В JavaScript нет встроенной поддержки приватных переменных, но замыкания позволяют эмулировать их. Вот пример счетчика:

function createCounter() {
  let count = 0;
  return {
    increment: function () {
      count++;
    },
    getCount: function () {
      return count;
    }
  };
}

const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // Выводит: 1
console.log(counter.count); // undefined

Здесь переменная count приватна и доступна только через методы increment и getCount. Это предотвращает случайное изменение count извне.

Паттерн модуля

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

const myModule = (function () {
  let privateVar = 'Я приватная';

  function privateMethod() {
    console.log(privateVar);
  }

  return {
    publicMethod: function () {
      privateMethod();
    }
  };
})();

myModule.publicMethod(); // Выводит: Я приватная
console.log(myModule.privateVar); // undefined

В этом примере privateVar и privateMethod недоступны извне, а publicMethod предоставляет контролируемый доступ.

Обработчики событий и коллбэки

Замыкания часто используются в асинхронном программировании, например, с таймерами или обработчиками событий:

function setupAlert() {
  const message = 'Время вышло!';
  setTimeout(function () {
    console.log(message);
  }, 1000);
}

setupAlert(); // Через 1 секунду выводит: Время вышло!

Здесь анонимная функция внутри setTimeout — это замыкание, которое помнит переменную message.

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

Одна из самых распространенных ошибок новичков связана с использованием замыканий в циклах. Рассмотрим пример:

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

Многие ожидают, что этот код выведет 0, 1, 2, но он выводит 3 три раза. Почему? Потому что переменная i, объявленная с var, имеет функциональную область видимости, и все замыкания внутри setTimeout ссылаются на одну и ту же переменную i. К моменту выполнения таймеров цикл завершен, и i равно 3. Есть два способа исправить это:

Использование IIFE (Immediately Invoked Function Expression):

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

Здесь IIFE создает новую область видимости для каждой итерации, фиксируя значение i.

Использование let:

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

Переменная let имеет блочную область видимости, поэтому каждая итерация цикла создает новую переменную i. Этот код выводит 0, 1, 2. Этот пример часто встречается на собеседованиях, поэтому важно его понять.

Замыкания с асинхронным кодом

Замыкания играют ключевую роль в асинхронном программировании. Помимо setTimeout, они используются в обработчиках событий и коллбэках. Вот пример с имитацией загрузки данных:

function createLogger(prefix) {
  return function (message) {
    console.log(prefix + ': ' + message);
  };
}

const infoLogger = createLogger('INFO');

function fetchData(callback) {
  setTimeout(() => {
    const data = {result: 'некоторые данные'};
    callback(data);
  }, 1000);
}

fetchData(function (data) {
  infoLogger('Данные получены: ' + JSON.stringify(data));
});

Здесь infoLogger — это замыкание, которое помнит prefix, а анонимная функция в fetchData — замыкание, использующее infoLogger.

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

Теперь рассмотрим более сложные применения замыканий.

Каррирование

Каррирование — это техника, при которой функция возвращает другую функцию, позволяя частично применять аргументы:

function multiply(a) {
  return function (b) {
    return a * b;
  };
}

const double = multiply(2);
console.log(double(5)); // Выводит: 10

Функция double — это замыкание, которое помнит значение a равное 2 и умножает на него любой переданный аргумент.

Мемоизация

Мемоизация — это техника кэширования результатов функции для повышения производительности:

function memoize(fn) {
  const cache = {};
  return function (...args) {
    const key = JSON.stringify(args);
    if (cache[key]) {
      return cache[key];
    } else {
      const result = fn(...args);
      cache[key] = result;
      return result;
    }
  };
}

function slowFunction(x) {
  for (let i = 0; i < 1e6; i++) {
  } // Имитация медленных вычислений
  return x * x;
}

const memoizedSlowFunction = memoize(slowFunction);
console.log(memoizedSlowFunction(5)); // Вычисляет и кэширует
console.log(memoizedSlowFunction(5)); // Берет из кэша

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

Вместо завершения: статья будет дополняться примерами и улучшаться по обратной связи.