Перейти к содержимому
Теория · Замыкания и this · урок 2 из 11

Замыкания

В прошлом уроке функция была карточкой с рецептом. Теперь прикрепим к карточке записку — переменную, с которой рецепт работает. Карточку унесли из кухни, где её написали, а записка осталась при ней.

Как вы уже знаете, функция видит переменные того места, где она написана: это лексическая область видимости. Значит, функция, написанная внутри другой функции, видит её переменные. Но внутреннюю функцию можно вернуть, и тогда внешняя функция завершится раньше, чем внутреннюю вызовут:

JavaScript
function makeGreeter(greeting) {
    return (name) => `${greeting}, ${name}`;
}

const hi = makeGreeter("Hi");
const hello = makeGreeter("Hello");

console.log(hi("Ann")); // Hi, Ann
console.log(hello("Bob")); // Hello, Bob

Давайте разберём по строкам:

  1. makeGreeter("Hi") получает "Hi" в параметр greeting, возвращает стрелочную функцию и завершается.
  2. const hi хранит эту стрелочную функцию.
  3. makeGreeter("Hello") делает то же со своим greeting, и результат попадает в hello.
  4. hi("Ann") печатает Hi, Ann, хотя вызов makeGreeter("Hi") давно закончился. Стрелочная функция всё ещё знает свой greeting.

Замыкание

Замыкание — функция вместе с переменными того места, где она создана. Возвращённая функция хранит не копию значения, а саму переменную greeting своего вызова makeGreeter. Эта переменная живёт, пока жива функция, которая её использует, хотя сам вызов makeGreeter уже закончился.

Каждый вызов makeGreeter создаёт свой параметр greeting и свою стрелочную функцию. Поэтому у hi и hello разные приветствия, и они не мешают друг другу: две карточки, у каждой своя записка.

Этот вопрос часто задают на собеседовании: «Что такое замыкание?» Хороший ответ состоит из двух частей:

  • функция запоминает переменные места, где её создали;
  • эти переменные живут и после того, как внешняя функция завершилась.

Счётчик

Замыкание может не только читать переменную, но и менять её. Тогда переменная хранит состояние между вызовами. Функцию, которая создаёт такие замыкания, называют фабрикой:

JavaScript
function makeCounter() {
    let count = 0;
    return () => {
        count += 1;
        return count;
    };
}

const next = makeCounter();
console.log(next()); // 1
console.log(next()); // 2
console.log(next()); // 3

Строка let count = 0 выполняется один раз, при вызове makeCounter. Вызовы next её не повторяют: они увеличивают тот же count, поэтому счёт растёт. Новый вызов makeCounter создал бы новую переменную count, и счёт новой функции снова начался бы с 1.

Здесь картинка с запиской нуждается в оговорке. Бумажная записка — копия: если потом исправить оригинал, записка останется прежней. Замыкание держит не копию, а саму переменную, поэтому каждое изменение count видит следующий вызов.

Нажмите «Следующий шаг» и следите за строкой «захвачена»: рамка вызова makeCounter исчезает, а count остаётся у замыкания и растёт.

ВживуюСчётчик из замыканияшаг 1 из 10 · рамок: 1

Программа вызывает makeCounter и ждёт, что та вернёт.

Строка «захвачена» — переменные, которые держит замыкание

Снаружи до count не добраться: в файле нет имени, по которому его можно прочитать. Изменить счёт можно только вызовом next, и случайно испортить его другой код не может.

Две функции из одного вызова

Две функции, созданные в одном и том же вызове, видят одни и те же переменные. Это одна записка, прикреплённая сразу к двум карточкам:

JavaScript
function makeScore() {
    let score = 0;
    return {
        add: (points) => { score += points; },
        get: () => score
    };
}

const game = makeScore();
game.add(5);
game.add(3);
console.log(game.get()); // 8

Разберём по шагам:

  1. Вызов makeScore создаёт одну переменную score и две стрелочные функции.
  2. Обе функции попадают в свойства add и get возвращённого объекта.
  3. game.add(5) и game.add(3) меняют ту самую score.
  4. game.get() читает её же и печатает 8.

Частая ошибка

Переменную счёта объявили внутри возвращаемой функции:

JavaScript
function makeCounter() {
    return () => {
        let count = 0; // ✗
        count += 1;
        return count;
    };
}

const next = makeCounter();
console.log(next(), next()); // 1 1

Ошибки нет, но счёт не растёт. Теперь let count = 0 выполняется при каждом вызове next, и каждый вызов начинает с нуля. Объявите count в теле makeCounter, перед return: тогда переменная одна на весь счётчик.

Главное

  • Замыкание — функция вместе с переменными места, где она создана; эти переменные живут и после конца внешнего вызова.
  • Замыкание держит саму переменную, а не копию, поэтому изменения сохраняются между вызовами.
  • Каждый вызов фабрики создаёт новые переменные, а функции из одного вызова делят одни и те же.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом