Замыкания
В прошлом уроке функция была карточкой с рецептом. Теперь прикрепим к карточке записку — переменную, с которой рецепт работает. Карточку унесли из кухни, где её написали, а записка осталась при ней.
Как вы уже знаете, функция видит переменные того места, где она написана: это лексическая область видимости. Значит, функция, написанная внутри другой функции, видит её переменные. Но внутреннюю функцию можно вернуть, и тогда внешняя функция завершится раньше, чем внутреннюю вызовут:
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Давайте разберём по строкам:
makeGreeter("Hi")получает"Hi"в параметрgreeting, возвращает стрелочную функцию и завершается.const hiхранит эту стрелочную функцию.makeGreeter("Hello")делает то же со своимgreeting, и результат попадает вhello.hi("Ann")печатаетHi, Ann, хотя вызовmakeGreeter("Hi")давно закончился. Стрелочная функция всё ещё знает свойgreeting.
Замыкание
Замыкание — функция вместе с переменными того места, где она создана. Возвращённая функция хранит не копию значения, а саму переменную greeting своего вызова makeGreeter. Эта переменная живёт, пока жива функция, которая её использует, хотя сам вызов makeGreeter уже закончился.
Каждый вызов makeGreeter создаёт свой параметр greeting и свою стрелочную функцию. Поэтому у hi и hello разные приветствия, и они не мешают друг другу: две карточки, у каждой своя записка.
Этот вопрос часто задают на собеседовании: «Что такое замыкание?» Хороший ответ состоит из двух частей:
- функция запоминает переменные места, где её создали;
- эти переменные живут и после того, как внешняя функция завершилась.
Счётчик
Замыкание может не только читать переменную, но и менять её. Тогда переменная хранит состояние между вызовами. Функцию, которая создаёт такие замыкания, называют фабрикой:
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 остаётся у замыкания и растёт.
1function makeCounter() {2 let count = 0;3 return () => {4 count += 1;5 return count;6 };7}8 9const next = makeCounter();10console.log(next());11console.log(next());
Программа вызывает makeCounter и ждёт, что та вернёт.
Снаружи до count не добраться: в файле нет имени, по которому его можно прочитать. Изменить счёт можно только вызовом next, и случайно испортить его другой код не может.
Две функции из одного вызова
Две функции, созданные в одном и том же вызове, видят одни и те же переменные. Это одна записка, прикреплённая сразу к двум карточкам:
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Разберём по шагам:
- Вызов
makeScoreсоздаёт одну переменнуюscoreи две стрелочные функции. - Обе функции попадают в свойства
addиgetвозвращённого объекта. game.add(5)иgame.add(3)меняют ту самуюscore.game.get()читает её же и печатает8.
Частая ошибка
Переменную счёта объявили внутри возвращаемой функции:
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 и подпишись на канал — откроются все уроки.