var: почему его больше не используют

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

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

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

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

let и const ограничены блоком — любыми фигурными скобками: телом if, циклом, просто парой {}. Для var блока не существует. Его область видимости — ближайшая функция, а если функции вокруг нет, то весь скрипт целиком.

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

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

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

Раз i одна на всю функцию, все функции, созданные внутри цикла, ссылаются на одну и ту же переменную. Вызовете их позже — каждая прочитает финальное значение, а не то, что было на её итерации.

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

Замените var i на let i — на каждой итерации появится собственная переменная, и вывод станет 0 1 2. Почему это работает именно так, разбирает статья «Замыкания: как функция помнит своё окружение».

Прежде чем выполнять функцию, движок собирает в ней все объявления var и создаёт имена заранее — со значением undefined. Присваивание остаётся на своей строке. Это и называют всплытием: имя как будто поднялось к началу функции, а значение осталось на месте. Подробно всплытие разобрано в статье «Всплытие и временная мёртвая зона» — здесь оно нужно как одно из отличий var.

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

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

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

Второй var с тем же именем — не ошибка, а обычное присваивание. В примере на четыре строки это выглядит безобидно; в файле на четыреста строк это способ незаметно затереть переменную, которую кто-то завёл двумястами строками выше.

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

let в такой ситуации останавливает вас ещё на разборе кода: SyntaxError: Identifier 'total' has already been declared. Файл вообще не запустится — и это хорошая новость, потому что о конфликте имён вы узнаёте до запуска, а не по странному поведению в проде.

Объявленная на верхнем уровне обычного скрипта, var не просто попадает в глобальную область — она становится свойством глобального объекта (window в браузере, globalThis в любой среде). let и const так не умеют: они тоже глобальные, но объект не засоряют.

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

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

Чем это плохо: глобальный объект — общая доска для всех скриптов страницы. Достаточно назвать переменную name, status или length — и вы переписываете то, что уже принадлежит браузеру или соседней библиотеке. Ломается при этом не ваш файл, а чужой.

Главное правило веба — не ломать старые страницы: сайт 2004 года должен открываться в сегодняшнем браузере. Поэтому поведение var оставили как есть, а рядом добавили let и const, куда сложили всё новое и строгое. По той же причине строгий режим не стал поведением по умолчанию для обычных скриптов — его включают явно или получают автоматически внутри модуля.

var живёт в коде старше 2015 года, в ответах на Stack Overflow, в примерах бумажных книг и в результатах сборки: транспиляторы до сих пор превращают let в var, когда целятся в древние браузеры. Ещё его любят маленькие скрипты, вставленные прямо в страницу без сборщика.

Читая такой код, держите в голове три поправки. Любое var внутри функции считайте объявленным в её первой строке. Фигурные скобки мысленно игнорируйте — на область видимости они не влияют. Встретив знакомое имя второй раз, проверьте, не перезаписали ли его случайно.

Отдельный узнаваемый приём — обёртка в немедленно вызываемую функцию, IIFE. До появления модулей это был единственный способ спрятать переменные от остальной страницы: раз область видимости у var функциональная, значит нужна функция.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Что сравниваемvarlet / const
Область видимостифункция или весь скриптблок в фигурных скобках
Обращение до объявленияundefinedReferenceError
Повторное объявление рядомразрешено молчаSyntaxError
Свойство globalThisсоздаётсяне создаётся

var не сломан — он просто из времён, когда скрипт на странице занимал тридцать строк, а не был приложением целиком.

  • Область видимости var — функция или весь скрипт; фигурные скобки блока его не удерживают.
  • Объявление всплывает: до своей строки переменная уже существует со значением undefined, а не бросает ошибку.
  • Повторное var с тем же именем — не ошибка, а тихая перезапись; let в такой ситуации даёт SyntaxError.
  • На верхнем уровне обычного скрипта var создаёт свойство globalThis (window) — let и const так не делают.
  • В новом коде: const по умолчанию, let когда значение меняется. var оставьте для чтения чужого и старого кода.

1/3Цикл for (var i = 0; i < 3; i++) {} отработал до конца. Что выведет console.log(i) на следующей строке?

2/3Строкой выше стоит console.log(x), строкой ниже — var x = 5. Что попадёт в консоль?

3/3В одной функции дважды написано var total: сначала со значением 10, потом со значением 99. Что сделает JavaScript?

Почините цикл, который отдаёт одно и то же

Функции создаются в цикле и должны запомнить номер своей итерации. Сейчас все три возвращают одну и ту же строку. Исправьте код так, чтобы handlers[0]() вернул «кнопка 0», а handlers[2]() — «кнопка 2».

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

Переменная i одна на всю функцию: цикл её трижды перезаписал и оставил значение 3. Все три замыкания смотрят в одну и ту же ячейку. Достаточно заменить var i на let i — тогда на каждой итерации создаётся своя переменная, и каждая функция запоминает именно её.

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

Если var трогать нельзя — например, вы правите старый файл, — тот же эффект даёт обёртка в немедленно вызываемую функцию: аргумент n становится отдельной переменной на каждом шаге. Так эту задачу решали до появления let.

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

Прочитайте функцию как движок

Не запуская код, скажите, что выведут оба console.log. Потом перепишите функцию на let так, чтобы её поведение читалось прямо из кода.

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

Выведется undefined, затем изменено. Оба var status относятся к одной переменной функции: имя создаётся до первой строки, поэтому первый вывод — undefined. Затем в неё кладут «готово», а var внутри if не создаёт новую переменную, а перезаписывает ту же самую значением «изменено».

На let первую строку пришлось бы убрать — обращение до объявления бросит ReferenceError, и это правильно: читать переменную раньше, чем ей что-то присвоили, смысла нет. Внутри if остаётся обычное присваивание.

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

Обратите внимание: код стал короче, а вопрос «а какая тут переменная в этой строке» просто исчез.