Замыкания
Вопрос
Что такое замыкание и почему цикл с var и setTimeout печатает одно и то же число?
Короткий ответ
Замыкание — это функция вместе с лексическим окружением, в котором она была создана. Внутренняя функция продолжает читать внешние переменные после того, как внешний вызов завершился.
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0)
}
// 3, 3, 3var одна на всю функцию. К моменту колбэков цикл уже закончился, i === 3. С let на каждой итерации своя привязка, и в консоли будет 0, 1, 2.
Разбор
Область видимости в JS лексическая: её задаёт место объявления, а не место вызова. Замыкание хранит ссылку на переменную, а не снимок значения. Если внешнюю let-переменную потом изменить, внутренние функции увидят новое значение.
Практические места: фабрика с приватным состоянием, обработчик, который помнит id, debounce, который помнит таймер, частично применённая функция.
function createCounter() {
let value = 0
return () => ++value
}Модуль ES тоже замыкает свои let и const: снаружи их нет, пока вы сами не вернёте функцию или объект.
На что смотрят на senior
Замыкание держит окружение живым. Если повесить обработчик на глобальный объект и забыть снять его, вместе с колбэком останется всё, на что он ссылается: большой массив, DOM-узел, экземпляр компонента. На разборе утечки это первый кандидат после «забыли отписаться».
В цикле рендера замыкание на каждой итерации — нормально. Проблема начинается, когда такое замыкание сохраняют в подписку без очистки.
Типичные ошибки
- Объяснять замыкание как «функция внутри функции» без лексического окружения.
- Думать, что замыкание копирует значение в момент создания.
- Лечить
varв цикле только через IIFE и не помнить проlet.
Задача
Сколько раз функция add создаёт новое окружение и что вернёт второй вызов?
function make(step) {
let total = 0
return () => {
total += step
return total
}
}
const add = make(2)
add()
add()Показать ответ
make(2) создаёт одно окружение с total и step. Обе стрелки — это один и тот же возвращённый функция-замыкание, она помнит total, а не копирует его.
Первый вызов вернёт 2, второй 4.