Всплытие и временная мёртвая зона

замыкания≈ 7 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

До того как выполнится первая строка, движок уже успел пройтись по вашему коду и записать, какие имена в нём встречаются. Отсюда и undefined там, где ждёшь ошибку, и ReferenceError на переменной, объявленной парой строк ниже.

Разделы темы:·

Кажется, что JavaScript выполняет код сверху вниз, строка за строкой. Почти: перед выполнением движок делает быстрый проход по области видимости (что это такое, разобрано в статье «Область видимости») и заводит в ней все имена, которые в ней объявлены — переменные, функции, классы. Только после этого начинается собственно выполнение.

Этот подготовительный проход и называют всплытием (hoisting). Важно, что имена заводятся все сразу, а вот значения в них попадают по-разному — и именно эта разница даёт все сюрпризы.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Переменная, объявленная через var, заводится на подготовительном проходе и сразу получает временное значение undefined. Присваивание же остаётся на своём месте и сработает, только когда выполнение до него дойдёт.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Не путайте это с обращением к имени, которого нет вообще: там будет честная ошибка. Разницу удобно увидеть через typeof — он единственный, кому позволено спрашивать про несуществующие имена.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

var всплывает до начала функции, а не блока. Поэтому переменная, объявленная внутри if, существует и снаружи — просто со значением undefined, если до присваивания дело не дошло.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

let и const тоже всплывают: имя резервируется в блоке с самого начала. Но значения — даже временного — у него нет, и любое обращение к переменной до строки объявления заканчивается ошибкой. Отрезок от начала блока до объявления называют временной мёртвой зоной (TDZ, temporal dead zone).

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Мёртвая зона — не «переменной ещё нет». Она есть, и потому перекрывает одноимённую внешнюю: обратиться к внешнему значению в блоке, где объявлен свой let, уже не получится. И typeof здесь тоже не спасает — он падает наравне с обычным обращением.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

«Всплытие» — метафора, код никуда не переезжает. Просто имена в области видимости заводятся раньше, чем выполнение до них доходит.

Объявление функции (function имя() {}) всплывает вместе с телом: к моменту выполнения функция полностью готова. Поэтому её можно вызвать выше по файлу, чем она написана, — из всего, что объявляется в самом файле, так умеет только она.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Функция-выражение — это обычное присваивание, просто справа стоит функция. Значит, действуют правила той переменной, в которую её кладут: у const и let это мёртвая зона со всеми последствиями.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Тогда ошибка сменит тип: имя уже заведено и содержит undefined, а вызвать undefined нельзя — получаем TypeError. Диагностика становится хуже: сообщение говорит «не функция» вместо «ещё не объявлено».

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Вопрос живучий, потому что за ним стоит не заучивание, а понимание: код не выполняется строго сверху вниз, области видимости готовятся заранее, а var, let и объявление функции ведут себя по-разному. Обычно просят предсказать вывод короткого фрагмента — и ошибаются как раз на том, что var даёт undefined, а let — исключение.

КонструкцияЧто до строки объявленияГде видна
var x = 1undefinedвся функция
let x = 1 и const x = 1ReferenceError (TDZ)свой блок
function f() {}готовая функциявся функция
const f = () => {}ReferenceError (TDZ)свой блок
class C {}ReferenceError (TDZ)свой блок

Практический вывод простой: пишите на const, а если значение меняется — на let, и объявляйте переменную прямо перед первым использованием. Тогда мёртвая зона остаётся страховкой от опечаток, а не источником загадок, и вопрос про всплытие превращается в вопрос про историю языка. Саму эту историю — почему var остался в языке и как читать код, где он встречается, — разбирает статья «var: почему его больше не используют».

  • Перед выполнением движок проходит по области видимости и заводит все объявленные в ней имена — это и называют всплытием.
  • var получает временное значение undefined, поэтому обращение до объявления не падает, а тихо возвращает undefined.
  • let, const и class тоже всплывают, но без значения: от начала блока до строки объявления они во временной мёртвой зоне, и обращение к ним даёт ReferenceError (typeof не спасает).
  • Объявление функции всплывает вместе с телом и вызывается выше по файлу; функция-выражение подчиняется правилам своей переменной — const даёт ReferenceError, var даёт TypeError.
  • Практика проста: const по умолчанию, let при необходимости, объявление — прямо перед первым использованием.

1/3Что выведет console.log(x); если следующей строкой идёт var x = 10;?

2/3А если то же самое, но с let x = 10;?

3/3Что произойдёт при вызове say(); если ниже написано var say = function () { console.log("хай"); };?

Почему функция падает

Этот код выбрасывает ошибку. Объясните, какую именно и почему, а затем предложите два разных способа починить его.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Ошибка — ReferenceError. Имя message принадлежит телу функции с самого начала её выполнения, но до строки let оно находится во временной мёртвой зоне, поэтому читать его нельзя.

Первый способ — объявить переменную до использования:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Второй — вынести значение в параметр, тогда оно существует к моменту входа в функцию:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

А вот замена let на var починкой не считается: ошибка исчезнет, но вместо неё выведется undefined — молчаливый баг вместо громкого.

Предскажите вывод

Не запуская код, скажите, что выведут обе строки внутри функции. Затем проверьте себя в консоли.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Выведется undefined, затем внутреннее.

Внутри show объявлена своя переменная label. Она всплывает до начала функции и полностью закрывает собой внешнюю — значит, первый console.log обращается уже к локальной, а в ней пока undefined. После присваивания во второй строке видно «внутреннее». Внешнее значение внутри функции недоступно ни в один момент.

С let вместо var первый console.log не вывел бы ничего — упал бы с ReferenceError. Это и есть польза мёртвой зоны: она превращает такую путаницу в явную ошибку.