Область видимости отвечает на вопрос «где эта переменная доступна». Замыкание отвечает на вопрос «почему функция видит переменную, когда внешняя функция уже давно завершилась». Это самый частый теоретический вопрос на JS-собесе.
Лексическая область видимости
В JavaScript область видимости лексическая: она определяется тем, где функция написана, а не тем, откуда её вызвали.
const name = 'внешняя';
function show() { console.log(name); }
function run() {
const name = 'из run';
show();
}
run(); // 'внешняя'
show объявлена на верхнем уровне, поэтому ищет name там, а не в run. Поиск идёт изнутри наружу по цепочке областей: текущая функция, внешняя, ещё внешняя, глобальная. Не нашли нигде: ReferenceError.
Какие бывают области
- Глобальная. Всё, что объявлено вне функций и блоков.
- Модульная. В ES-модуле верхний уровень свой, переменные не попадают в глобальный объект.
- Функциональная. Каждый вызов функции создаёт новую область.
- Блочная.
{ ... }, телоif,for,try. В ней живут толькоlet,constиclass.
var, let, const
| var | let | const | |
|---|---|---|---|
| Область | функция | блок | блок |
| Повторное объявление | можно | нельзя | нельзя |
| Переприсваивание | можно | можно | нельзя |
| До объявления | undefined | ReferenceError (TDZ) | ReferenceError (TDZ) |
| На верхнем уровне скрипта | свойство window | нет | нет |
const запрещает переприсвоить переменную, но не делает объект неизменяемым: const user = {}; user.name = 'Аня' работает. Для заморозки есть Object.freeze, и он поверхностный.
Hoisting и TDZ
Перед выполнением функции движок проходит по её телу и регистрирует все объявления. Это называют подъёмом (hoisting).
function f() {}поднимается целиком: функцию можно вызвать выше по коду.var xподнимается и сразу получаетundefined.let,const,classтоже регистрируются, но до строки объявления находятся во временной мёртвой зоне (TDZ). Любое обращение к ним, дажеtypeof x, бросает ReferenceError.
console.log(a); // undefined
var a = 1;
console.log(b); // ReferenceError
let b = 2;
TDZ полезна: она ловит обращение к переменной, которая ещё не готова, вместо тихого undefined.
Замыкание
Замыкание это функция вместе с лексическим окружением, в котором она создана. Функция хранит ссылку на это окружение и видит его переменные, даже когда внешняя функция завершилась.
function createCounter() {
let count = 0;
return {
inc: () => ++count,
get: () => count,
};
}
const c = createCounter();
c.inc(); c.inc();
c.get(); // 2
Важно: замыкание хранит ссылку на переменную, а не копию значения. Если переменную изменить после создания функции, функция увидит новое значение. Каждый вызов createCounter создаёт новое окружение, поэтому два счётчика независимы.
Технически в JS замыканием является любая функция: каждая запоминает своё окружение. На собесе обычно имеют в виду функцию, которая использует переменные внешней функции после её завершения.
Где замыкания используются
- Приватное состояние: счётчик выше, модульный паттерн.
- Фабрики функций:
const add5 = makeAdder(5). - Колбэки и обработчики: обработчик клика помнит id элемента.
debounce,throttle,memoize: таймер или кеш живёт в замыкании.- Хуки React: каждый рендер создаёт новые функции, которые замыкают значения этого рендера.
Классическая ловушка: цикл
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i)); // 3 3 3
}
var одна на всю функцию, к моменту запуска колбэков она уже 3. С let спецификация создаёт новую привязку i на каждой итерации, и выводится 0 1 2. До ES6 то же самое делали через IIFE, передавая i аргументом.
Устаревшие замыкания (stale closure)
Функция видит переменные того окружения, где создана. Если окружение потом заменили новым (как в каждом рендере React), старая функция продолжит видеть старые значения. Отсюда баги с setInterval внутри useEffect без зависимостей: колбэк всегда видит первый count.
Замыкания и память
Пока жива функция, живо и её окружение. Если обработчик замыкает большой массив и не удалён, массив не соберёт сборщик мусора. Современные движки сохраняют только реально используемые переменные, но полагаться на это в деталях не стоит.
Частые ошибки
- Считать, что замыкание копирует значение в момент создания.
- Путать «где вызвана» и «где объявлена» функция.
- Ждать, что
constсделает объект неизменяемым. - Думать, что
letне поднимается. Поднимается, но в TDZ.
Что спросят на собесе
- Что такое замыкание, пример из практики.
- Чем отличаются
var,let,const, что такое hoisting и TDZ. - Почему цикл с
varиsetTimeoutвыводит одно и то же число и как исправить. - Что такое лексическая область видимости.
- Как замыкания связаны с утечками памяти и устаревшими значениями в React.