← назад к разделу

Область видимости (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 из примера сработал после конца цикла.