Область видимости
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Область видимости — это ответ на вопрос «откуда эту переменную видно». Пока в файле десять строк, вопрос кажется праздным; в сотне строк именно он решает, работает программа или ломается в самом неожиданном месте.
Всё, что объявлено на верхнем уровне скрипта — вне функций и вне фигурных скобок, — попадает в глобальную область видимости. Такую переменную видно из любой точки программы: из функции, из цикла, из функции внутри функции.
Звучит удобно — и в этом ловушка. Глобальная переменная похожа на общую тетрадь: писать в неё может кто угодно, а потом ищи, какая из тридцати функций испортила значение. Поэтому наверху держат только то, что действительно нужно всем: константы, настройки, точку входа.
Функция — непрозрачная коробка. Всё, что объявлено внутри неё, включая параметры, снаружи не существует. Не «равно undefined», а именно не существует: обращение к такому имени даёт ReferenceError.
В этом — вторая польза функций после переиспользования кода. Временные переменные не засоряют программу и не сталкиваются именами с чужими: внутри makeTotal можно завести total, даже если такое имя уже занято в соседней функции.
Блок — это пара фигурных скобок: тело if, тело цикла или просто { } сами по себе. Переменные, которые мы заводили в статье «Переменные: let, const и почему не var», внутри блока живут только до закрывающей скобки. А вот var блока не замечает — она принадлежит всей функции целиком.
Попытка обратиться к blockOnly после закрывающей скобки закончится ошибкой. Особенно наглядно это в цикле: счётчик, объявленный через let, исчезает вместе с циклом и не мешает коду ниже.
Функции можно вкладывать друг в друга, и области видимости вкладываются вместе с ними, как матрёшки. Встретив имя, движок ищет его в текущей области; не нашёл — поднимается на уровень наружу; и так до глобальной. Эта лестница и называется цепочкой областей видимости.
Направление поиска всегда одно — наружу. Изнутри окно наружу есть, снаружи внутрь — нет: outer не увидит переменные, объявленные в inner. А если имя не нашлось нигде, включая глобальную область, программа падает с ReferenceError.
Цепочка складывается в тот момент, когда вы пишете функцию, а не когда вызываете её. Функция всегда смотрит наружу от того места, где объявлена, — и никакой вызов этого не изменит. На этом держатся замыкания: им посвящена статья «Замыкания: как функция помнит своё окружение».
Что если внутреннее имя совпадёт с внешним? Ничего страшного: поиск останавливается на первом совпадении, то есть на ближайшем. Внутренняя переменная затеняет внешнюю — та никуда не девается, просто на время недоступна под этим именем.
Чаще всего затеняют параметры — и это нормально, пока вы делаете это осознанно. Плохо, когда затенение случайно: кажется, что меняешь внешнее значение, а меняешь местную копию имени, и внешняя переменная остаётся прежней.
Всё, что мы видели про var, сводится к одному: она игнорирует блоки. Объявленная в if или в цикле, она принадлежит всей функции — и переживает блок, в котором родилась, вместе со своим последним значением.
Второе отличие — момент рождения. var создаётся в самом начале работы функции со значением undefined, поэтому обращение «до объявления» молча вернёт undefined. С let и const то же обращение сразу даёт ошибку — и это подарок: она показывает опечатку в ту же секунду, а не через час отладки.
Переменная let существует с начала блока, но до строки объявления её нельзя ни читать, ни записывать. Этот отрезок называют временной мёртвой зоной (TDZ), и живёт в ней только ошибка. Откуда она берётся и чем отличается от тихого undefined у var, разбирает следующая статья — «Всплытие и временная мёртвая зона».
Правило, которое снимает почти все вопросы: объявляй переменную там, где она нужна, и ни строчкой выше.
- Область видимости — это участок кода, из которого переменная доступна: глобальная, функциональная и блочная.
- Функция и блок видят наружу, но не внутрь: снаружи локальные переменные не существуют, обращение к ним даёт ReferenceError.
- Поиск имени идёт по цепочке областей от ближайшей к глобальной и останавливается на первом совпадении.
- Совпадение имён не ошибка: внутренняя переменная затеняет внешнюю, оставляя её значение нетронутым.
- var игнорирует блоки и молча возвращает undefined до объявления; let и const ограничены блоком и падают с понятной ошибкой — поэтому по умолчанию используют их.
1/3Что выведет последняя строка? let n = 1; function f() { let n = 2; } f(); console.log(n);
2/3Переменная объявлена через var внутри блока if. Откуда она видна?
3/3Функция обращается к имени, которого нет ни внутри неё, ни в областях снаружи. Что произойдёт?
Спрячьте переменную внутри функции
Токен не должен быть доступен никому, кроме функции auth. Сейчас он лежит в глобальной области, и последняя строка спокойно его печатает. Перепишите код так, чтобы внутри функции всё работало, а снаружи обращение к token давало ReferenceError.
Достаточно перенести объявление внутрь функции: тогда token живёт в её области видимости и снаружи такого имени просто нет.
Это общее правило: если значение нужно одной функции — объявляйте его там же. Глобальная область не хранилище, а витрина.
Предскажите вывод
Три объявления с одним именем на трёх уровнях вложенности. Не запуская код, скажите, что напечатают три вызова console.log — и объясните, почему именно так.
Вывод: C, затем B, затем A.
Каждый console.log берёт ближайшую переменную v из своей области видимости. Внутри блока if ближайшая — та, что объявлена в этом блоке (C). После закрывающей скобки блочная переменная умирает, и видна переменная функции (B). После вызова demo остаётся только глобальная (A) — затенение её не изменило, а лишь временно перекрыло.