Урок по треку JavaScript 7 мин

Области видимости и замыкания за 7 минут

var, let, const, hoisting, TDZ и как функция запоминает своё окружение.

Область видимости отвечает на вопрос «где эта переменная доступна». Замыкание отвечает на вопрос «почему функция видит переменную, когда внешняя функция уже давно завершилась». Это самый частый теоретический вопрос на 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

varletconst
Областьфункцияблокблок
Повторное объявлениеможнонельзянельзя
Переприсваиваниеможноможнонельзя
До объявленияundefinedReferenceError (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.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.

Вопросы модуля (36)