$ sudo teach IT
РАЗДЕЛ 6 · УРОК 5

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

Почему переменная, объявленная внутри функции, недоступна снаружи? Почему можно вызвать function declaration до её объявления? Всё это объясняет область видимости и механизм hoisting — «поднятия».

⏱ ~30 минут 🎓 Для новичков 🟡 JavaScript

🔭 Область видимости: где «видна» переменная

Область видимости (scope) — это часть кода, где переменная или функция доступна. Представьте здание с комнатами: вещи в комнате доступны только тем, кто находится в этой комнате. Вещи в коридоре доступны всем.

В JavaScript есть несколько уровней видимости. Начнём с самых базовых.

🌍 Глобальная область видимости

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

let globalVar = "Я глобальная!"; // глобальная переменная

function showGlobal() {
  console.log(globalVar); // "Я глобальная!" — доступна внутри функции
}

function modifyGlobal() {
  globalVar = "Изменена!"; // можно изменять изнутри
}

showGlobal();   // "Я глобальная!"
modifyGlobal();
showGlobal();   // "Изменена!"
console.log(globalVar); // "Изменена!"

⚠️ Предупреждение: глобальные переменные — это удобно, но опасно. Любая функция может их изменить. В больших программах это приводит к неожиданным ошибкам — трудно отследить, кто изменил переменную. Старайтесь минимизировать использование глобальных переменных.

📦 Локальная (функциональная) область видимости

Переменные, объявленные внутри функции, доступны только в этой функции и вложенных в неё функциях. Снаружи они недоступны — как вещи в закрытой комнате.

function calculateBMI(weight, height) {
  let bmi = weight / (height * height); // локальная переменная
  let category;

  if (bmi < 18.5) {
    category = "Недостаточный вес";
  } else if (bmi < 25) {
    category = "Норма";
  } else {
    category = "Избыточный вес";
  }

  return "ИМТ: " + bmi.toFixed(1) + " (" + category + ")";
}

console.log(calculateBMI(70, 1.75)); // "ИМТ: 22.9 (Норма)"

// Попытка получить локальную переменную снаружи:
console.log(bmi);      // ❌ ReferenceError: bmi is not defined
console.log(category); // ❌ ReferenceError: category is not defined

Это хорошая вещь! Локальные переменные — приватные для функции. Разные функции могут использовать одинаковые имена переменных без конфликтов:

function calculateSquare(n) {
  let result = n * n; // своя переменная result
  return result;
}

function calculateCube(n) {
  let result = n * n * n; // своя переменная result — не конфликтует!
  return result;
}

console.log(calculateSquare(4)); // 16
console.log(calculateCube(3));   // 27
// Оба result — разные переменные в разных областях видимости

🧱 Блочная область видимости: let и const

let и const имеют блочную область видимости — они «живут» только внутри блока { }, в котором объявлены. Блок — это любые фигурные скобки: if, for, while, просто {}.

if (true) {
  let blockVar = "Я внутри блока";
  const blockConst = "Я тоже внутри";
  console.log(blockVar);   // "Я внутри блока" — OK
  console.log(blockConst); // "Я тоже внутри" — OK
}

console.log(blockVar);   // ❌ ReferenceError: blockVar is not defined
console.log(blockConst); // ❌ ReferenceError: blockConst is not defined
// В цикле for — i доступна только внутри цикла
for (let i = 0; i < 3; i++) {
  console.log(i); // 0, 1, 2
}

console.log(i); // ❌ ReferenceError: i is not defined

⚠️ var vs let: почему var опасен

До ES6 все переменные объявлялись через var. Проблема var: у него нет блочной области видимости! var «вытекает» из блоков — это очень неинтуитивно и приводит к ошибкам.

// var НЕ имеет блочной области видимости:
if (true) {
  var leaked = "Я утекла!";
}
console.log(leaked); // "Я утекла!" — var вытек из блока if!

// В цикле:
for (var i = 0; i < 3; i++) {
  // что-то делаем
}
console.log(i); // 3 — i вытекла из цикла!

🚫 Вывод: не используй var в современном коде. Всегда используй let (для изменяемых переменных) и const (для неизменяемых). var остался в языке для обратной совместимости, но новый код на нём не пишут.

У var есть функциональная область видимости — он «видит» только функцию, но не блок. Единственный «сдерживающий» его барьер — фигурные скобки функции:

function example() {
  if (true) {
    var x = 10; // var — функциональная область видимости
    let y = 20; // let — блочная область видимости
  }
  console.log(x); // 10 — var доступна в функции
  console.log(y); // ❌ ReferenceError — let не доступна
}

🚀 Hoisting: «поднятие» объявлений

Hoisting (поднятие) — это механизм JavaScript, при котором объявления функций и переменных «поднимаются» наверх своей области видимости ещё до выполнения кода. Не физически — просто интерпретатор обрабатывает их в первую очередь.

// Можно вызвать function declaration ДО объявления!
sayHello(); // "Привет!" — работает!

function sayHello() {
  console.log("Привет!");
}
// JavaScript "поднял" объявление функции наверх файла

Что реально происходит — JavaScript как будто переписывает код так:

// Что видит разработчик:
sayHello();
function sayHello() { console.log("Привет!"); }

// Что "видит" JavaScript после hoisting:
function sayHello() { console.log("Привет!"); } // поднято наверх
sayHello(); // теперь функция уже объявлена — вызов работает

💡 Hoisting объявлений функций — одна из причин, почему function declaration очень удобны. Можно организовать код так, чтобы главная логика была наверху файла, а вспомогательные функции — внизу. Читать удобнее.

🚧 Temporal Dead Zone: let, const и function expression

let, const и function expression тоже «поднимаются», но с одним важным отличием: они не инициализируются. Доступ к ним до строки объявления вызывает ошибку — они находятся в Temporal Dead Zone (TDZ).

// Function Expression — нельзя вызвать до строки с const
greet("Иван"); // ❌ ReferenceError: Cannot access 'greet' before initialization
const greet = function(name) {
  console.log("Привет, " + name);
};

// Стрелочная функция — то же самое
double(5); // ❌ ReferenceError
const double = n => n * 2;

// var с function expression — особый случай:
sayHi(); // ❌ TypeError: sayHi is not a function
var sayHi = function() { console.log("Hi!"); };
// var поднимается, но БЕЗ значения — sayHi = undefined до строки присваивания

Сводная таблица поведения при hoisting

Тип Поднимается? Доступно до строки?
function declaration ✅ Да, полностью ✅ Да
var ✅ Да, но без значения ⚠️ Да, но = undefined
let ✅ Да (TDZ) ❌ Нет (ReferenceError)
const ✅ Да (TDZ) ❌ Нет (ReferenceError)

🔗 Цепочка областей видимости (Scope Chain)

Когда JavaScript встречает переменную, он ищет её по цепочке: сначала в текущей области видимости, затем в внешней, затем ещё выше — вплоть до глобальной.

let globalName = "глобальная";

function outer() {
  let outerName = "внешняя";

  function inner() {
    let innerName = "внутренняя";

    // inner видит все три переменные:
    console.log(innerName);  // "внутренняя" — своя
    console.log(outerName);  // "внешняя" — из outer
    console.log(globalName); // "глобальная" — из глобальной области
  }

  // outer видит только свою и глобальную:
  console.log(outerName);  // "внешняя" — своя
  console.log(globalName); // "глобальная" — из глобальной области
  // console.log(innerName); // ❌ ReferenceError — не видит inner-переменную

  inner();
}

outer();

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

✅ Итоги урока

  • Глобальная область видимости — переменные вне функций и блоков, доступны везде
  • Локальная (функциональная) — переменные внутри функции, не видны снаружи
  • Блочная — let и const видны только в блоке {}, где объявлены
  • var не имеет блочной области видимости — «вытекает» из блоков. Не используй var
  • Hoisting: объявления function declaration поднимаются наверх — их можно вызвать до объявления
  • let и const в TDZ — обращение до строки объявления вызывает ReferenceError
  • Цепочка областей видимости: поиск переменной идёт наверх — от локальной к глобальной

В следующем уроке разберём замыкания — один из самых интересных и мощных паттернов JavaScript, который напрямую вырастает из области видимости. 🚀

Тест: Область видимости и hoisting

7 вопросов

Блочная vs функциональная область видимости

Напишите функцию scopeDemo, которая демонстрирует разницу между let и var. Внутри блока if(true) объявите переменную let x = 10 и var y = 20. Функция должна вернуть строку вида 'x: ОШИБКА, y: 20', где x недоступна снаружи блока. Оберните в try/catch для перехвата ошибки доступа к x.

Hoisting function declaration

Напишите функцию hoistTest, которая вызывает другую функцию innerHelper до её объявления (используя hoisting). innerHelper должна возвращать строку 'hoisted!'. Функция hoistTest должна вернуть результат вызова innerHelper.