Всплытие и временная мёртвая зона
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
До того как выполнится первая строка, движок уже успел пройтись по вашему коду и записать, какие имена в нём встречаются. Отсюда и undefined там, где ждёшь ошибку, и ReferenceError на переменной, объявленной парой строк ниже.
Кажется, что JavaScript выполняет код сверху вниз, строка за строкой. Почти: перед выполнением движок делает быстрый проход по области видимости (что это такое, разобрано в статье «Область видимости») и заводит в ней все имена, которые в ней объявлены — переменные, функции, классы. Только после этого начинается собственно выполнение.
Этот подготовительный проход и называют всплытием (hoisting). Важно, что имена заводятся все сразу, а вот значения в них попадают по-разному — и именно эта разница даёт все сюрпризы.
Переменная, объявленная через var, заводится на подготовительном проходе и сразу получает временное значение undefined. Присваивание же остаётся на своём месте и сработает, только когда выполнение до него дойдёт.
Не путайте это с обращением к имени, которого нет вообще: там будет честная ошибка. Разницу удобно увидеть через typeof — он единственный, кому позволено спрашивать про несуществующие имена.
var всплывает до начала функции, а не блока. Поэтому переменная, объявленная внутри if, существует и снаружи — просто со значением undefined, если до присваивания дело не дошло.
let и const тоже всплывают: имя резервируется в блоке с самого начала. Но значения — даже временного — у него нет, и любое обращение к переменной до строки объявления заканчивается ошибкой. Отрезок от начала блока до объявления называют временной мёртвой зоной (TDZ, temporal dead zone).
Мёртвая зона — не «переменной ещё нет». Она есть, и потому перекрывает одноимённую внешнюю: обратиться к внешнему значению в блоке, где объявлен свой let, уже не получится. И typeof здесь тоже не спасает — он падает наравне с обычным обращением.
«Всплытие» — метафора, код никуда не переезжает. Просто имена в области видимости заводятся раньше, чем выполнение до них доходит.
Объявление функции (function имя() {}) всплывает вместе с телом: к моменту выполнения функция полностью готова. Поэтому её можно вызвать выше по файлу, чем она написана, — из всего, что объявляется в самом файле, так умеет только она.
Функция-выражение — это обычное присваивание, просто справа стоит функция. Значит, действуют правила той переменной, в которую её кладут: у const и let это мёртвая зона со всеми последствиями.
Тогда ошибка сменит тип: имя уже заведено и содержит undefined, а вызвать undefined нельзя — получаем TypeError. Диагностика становится хуже: сообщение говорит «не функция» вместо «ещё не объявлено».
Вопрос живучий, потому что за ним стоит не заучивание, а понимание: код не выполняется строго сверху вниз, области видимости готовятся заранее, а var, let и объявление функции ведут себя по-разному. Обычно просят предсказать вывод короткого фрагмента — и ошибаются как раз на том, что var даёт undefined, а let — исключение.
| Конструкция | Что до строки объявления | Где видна |
|---|---|---|
| var x = 1 | undefined | вся функция |
| let x = 1 и const x = 1 | ReferenceError (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("хай"); };?
Почему функция падает
Этот код выбрасывает ошибку. Объясните, какую именно и почему, а затем предложите два разных способа починить его.
Ошибка — ReferenceError. Имя message принадлежит телу функции с самого начала её выполнения, но до строки let оно находится во временной мёртвой зоне, поэтому читать его нельзя.
Первый способ — объявить переменную до использования:
Второй — вынести значение в параметр, тогда оно существует к моменту входа в функцию:
А вот замена let на var починкой не считается: ошибка исчезнет, но вместо неё выведется undefined — молчаливый баг вместо громкого.
Предскажите вывод
Не запуская код, скажите, что выведут обе строки внутри функции. Затем проверьте себя в консоли.
Выведется undefined, затем внутреннее.
Внутри show объявлена своя переменная label. Она всплывает до начала функции и полностью закрывает собой внешнюю — значит, первый console.log обращается уже к локальной, а в ней пока undefined. После присваивания во второй строке видно «внутреннее». Внешнее значение внутри функции недоступно ни в один момент.
С let вместо var первый console.log не вывел бы ничего — упал бы с ReferenceError. Это и есть польза мёртвой зоны: она превращает такую путаницу в явную ошибку.