Область видимости (scope) отвечает на вопрос «откуда видна эта переменная». В JavaScript она лексическая — определяется тем, где переменная написана в коде, а не тем, откуда её пытаются прочитать во время выполнения. Это простое правило, но вокруг него наслоились три исторических сюжета: var, поднятие и мёртвая зона. Разберём все.
Лексическая вложенность
Области видимости вкладываются как матрёшки: функция видит свои переменные, переменные функции-родителя и глобальные — но не наоборот.
const outer = "внешняя";
function parent() {
const middle = "средняя";
function child() {
const inner = "внутренняя";
console.log(outer, middle, inner); // видит все три
}
child();
// console.log(inner); // ReferenceError — внутрь заглянуть нельзя
}
Поиск имени идёт изнутри наружу: сначала своя область, потом родительская, и так до глобальной. Первое найденное имя побеждает — внутренняя переменная затеняет (shadowing) внешнюю с тем же именем.
var против let/const: функция против блока
var — довоенный способ объявления, и его область — вся функция, а не блок:
function demo() {
if (true) {
var v = 1;
let l = 2;
}
console.log(v); // 1 — var «вытек» из блока
console.log(l); // ReferenceError — let живёт только в блоке
}
let и const — блочные: живут внутри ближайших {}. Разница между ними одна: const запрещает переприсваивание. Важный нюанс: const не делает объект неизменяемым — менять содержимое можно, нельзя лишь перенаправить ссылку:
const arr = [1, 2];
arr.push(3); // можно — меняем содержимое
arr = []; // TypeError — нельзя перенаправить
Современное правило: const по умолчанию, let — когда переприсваивание действительно нужно, var — никогда.
Поднятие: почему код видит переменные «до» объявления
Перед выполнением движок сканирует область и «узнаёт» обо всех объявлениях заранее — это называют поднятием (hoisting). Но поднимаются объявления по-разному:
console.log(v); // undefined — var поднят и инициализирован undefined
var v = 1;
console.log(l); // ReferenceError! — let поднят, но НЕ инициализирован
let l = 2;
hi(); // "привет" — функция-объявление поднята ЦЕЛИКОМ
function hi() { console.log("привет"); }
bye(); // TypeError — поднялась только переменная-const, не значение
const bye = () => console.log("пока");
Зона от начала блока до строки let/const называется временной мёртвой зоной (TDZ): переменная уже «существует», но читать её — ошибка. Это осознанный дизайн: лучше упасть с ReferenceError, чем молча получить undefined, как с var.
Практический вывод: function declaration можно вызывать до объявления (удобно класть главную функцию файла наверх, помощников — вниз); стрелочные функции в const — только после.
Классика: var в цикле
Легендарная задачка, которая ловила поколения разработчиков:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 10);
}
// 3, 3, 3 — а не 0, 1, 2!
Почему: var i — одна переменная на весь цикл (функциональная область). Все три колбэка замкнулись на неё одну, а к моменту срабатывания таймеров цикл закончился и i === 3.
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 10);
}
// 0, 1, 2 — let создаёт НОВУЮ переменную на каждую итерацию
Это не магия таймеров, а чистый scope: let в заголовке цикла означает «свежая переменная на каждый виток». Одна строчка — два разных мира.
Модули и глобальная область
Каждый ES-модуль (файл с import/export) — своя область видимости: переменные верхнего уровня файла не попадают в глобальную. Глобальную область сегодня загрязняют разве что скрипты без модулей и присваивания без объявления (в нестрогом режиме x = 1 молча создаст глобальную переменную — ещё одна причина, почему модули и классы всегда работают в строгом режиме).
Коротко
- Scope лексический: важно, где переменная написана. Поиск имён — изнутри наружу, внутренняя затеняет внешнюю.
var— область функции,let/const— блока. Правило:constпо умолчанию,letпо необходимости,var— никогда.constзапрещает переприсваивание, но не изменение содержимого объекта.- Поднятие: function declaration доступна до объявления целиком;
let/constдо строки объявления — мёртвая зона (ReferenceError);varдаёт молчаливыйundefined. varв цикле — одна переменная на все итерации,let— новая на каждую. Отсюда3, 3, 3против0, 1, 2.
Что почитать дальше
- Замыкания — что происходит, когда функция уносит область видимости с собой.
- Приведение типов — предыдущая тема цикла.
- Event loop — почему setTimeout из примера сработал после конца цикла.