Область видимости

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

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

Область видимости — это ответ на вопрос «откуда эту переменную видно». Пока в файле десять строк, вопрос кажется праздным; в сотне строк именно он решает, работает программа или ломается в самом неожиданном месте.

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

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

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

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

Функция — непрозрачная коробка. Всё, что объявлено внутри неё, включая параметры, снаружи не существует. Не «равно undefined», а именно не существует: обращение к такому имени даёт ReferenceError.

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

В этом — вторая польза функций после переиспользования кода. Временные переменные не засоряют программу и не сталкиваются именами с чужими: внутри makeTotal можно завести total, даже если такое имя уже занято в соседней функции.

Блок — это пара фигурных скобок: тело if, тело цикла или просто { } сами по себе. Переменные, которые мы заводили в статье «Переменные: let, const и почему не var», внутри блока живут только до закрывающей скобки. А вот var блока не замечает — она принадлежит всей функции целиком.

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

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

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

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

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

Направление поиска всегда одно — наружу. Изнутри окно наружу есть, снаружи внутрь — нет: outer не увидит переменные, объявленные в inner. А если имя не нашлось нигде, включая глобальную область, программа падает с ReferenceError.

Цепочка складывается в тот момент, когда вы пишете функцию, а не когда вызываете её. Функция всегда смотрит наружу от того места, где объявлена, — и никакой вызов этого не изменит. На этом держатся замыкания: им посвящена статья «Замыкания: как функция помнит своё окружение».

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

Что если внутреннее имя совпадёт с внешним? Ничего страшного: поиск останавливается на первом совпадении, то есть на ближайшем. Внутренняя переменная затеняет внешнюю — та никуда не девается, просто на время недоступна под этим именем.

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

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

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

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

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

Второе отличие — момент рождения. var создаётся в самом начале работы функции со значением undefined, поэтому обращение «до объявления» молча вернёт undefined. С let и const то же обращение сразу даёт ошибку — и это подарок: она показывает опечатку в ту же секунду, а не через час отладки.

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

Переменная let существует с начала блока, но до строки объявления её нельзя ни читать, ни записывать. Этот отрезок называют временной мёртвой зоной (TDZ), и живёт в ней только ошибка. Откуда она берётся и чем отличается от тихого undefined у var, разбирает следующая статья — «Всплытие и временная мёртвая зона».

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

Правило, которое снимает почти все вопросы: объявляй переменную там, где она нужна, и ни строчкой выше.

  • Область видимости — это участок кода, из которого переменная доступна: глобальная, функциональная и блочная.
  • Функция и блок видят наружу, но не внутрь: снаружи локальные переменные не существуют, обращение к ним даёт 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.

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

Достаточно перенести объявление внутрь функции: тогда token живёт в её области видимости и снаружи такого имени просто нет.

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

Это общее правило: если значение нужно одной функции — объявляйте его там же. Глобальная область не хранилище, а витрина.

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

Три объявления с одним именем на трёх уровнях вложенности. Не запуская код, скажите, что напечатают три вызова console.log — и объясните, почему именно так.

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

Вывод: C, затем B, затем A.

Каждый console.log берёт ближайшую переменную v из своей области видимости. Внутри блока if ближайшая — та, что объявлена в этом блоке (C). После закрывающей скобки блочная переменная умирает, и видна переменная функции (B). После вызова demo остаётся только глобальная (A) — затенение её не изменило, а лишь временно перекрыло.