Навигация

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

Область видимости (scope) определяет, где переменная доступна, а hoisting — как движок обрабатывает объявления до выполнения кода. Эти механизмы объясняют множество «странных» примеров на собеседованиях и лежат в основе замыканий.

Содержание

  1. Виды области видимости
  2. var, let, const
  3. Hoisting и Temporal Dead Zone
  4. Лексическое окружение
  5. Function declaration vs function expression
  6. Hoisting классов
  7. Практические примеры: что выведет код
  8. Ключевые тезисы для интервью
  9. Заключение
  10. Полезные ссылки

Виды области видимости

В JavaScript есть три уровня области видимости:

  1. Глобальная — переменная объявлена вне любой функции или блока. Доступна везде (в браузере становится свойством window, в Node.js — global).
  2. Функциональная — переменная доступна внутри функции, где объявлена, включая вложенные функции.
  3. Блочная — переменная доступна только внутри блока {}, где объявлена: if, for, while, функции-стрелки и т.д.
const globalVar = 'global';

function outer() {
  const functionVar = 'function';

  if (true) {
    const blockVar = 'block';
    console.log(functionVar); // 'function'
    console.log(globalVar);   // 'global'
  }

  console.log(blockVar); // ReferenceError
}

var, let, const

var

var — устаревший способ объявления. Переменные var имеют функциональную область видимости и поднимаются в начало функции или скрипта:

function example() {
  console.log(x); // undefined, а не ошибка
  var x = 10;
  console.log(x); // 10
}

Под капотом движок видит это так:

function example() {
  var x;          // объявление поднято
  console.log(x); // undefined
  x = 10;         // инициализация остаётся на месте
  console.log(x); // 10
}

var допускает повторное объявление в одной области видимости и не создаёт блочную область:

var x = 1;
if (true) {
  var x = 2;
}
console.log(x); // 2

let и const

let и const имеют блочную область видимости. Они тоже поднимаются, но попадают в Temporal Dead Zone (TDZ) — временную мёртвую зону — до момента инициализации:

function example() {
  console.log(y); // ReferenceError: Cannot access 'y' before initialization
  let y = 10;
}

Разница между let и const:

  • let позволяет переприсваивать значение;
  • const фиксирует ссылку на значение. Сам объект можно изменять, но переменную нельзя перезаписать.
const arr = [1, 2];
arr.push(3);      // OK
arr = [1, 2, 3]; // TypeError

Hoisting и Temporal Dead Zone

Hoisting — это не «перемещение» кода вверх, а результат двухфазной компиляции JavaScript:

  1. Фаза компиляции/подготовки. Движок проходит по коду, находит все объявления переменных и функций, создаёт лексические окружения.
  2. Фаза выполнения. Движок выполняет код, используя уже созданные окружения.

Поэтому var доступен до инициализации со значением undefined, а let/const — нет: они находятся в TDZ.

console.log(a); // undefined
var a = 5;

console.log(b); // ReferenceError
let b = 10;

TDZ существует не только для переменных, но и для параметров функций со значениями по умолчанию:

function foo(a = b, b) {}
foo(undefined, 1); // ReferenceError: Cannot access 'b' before initialization

Лексическое окружение

Каждый блок кода, функция или скрипт имеет лексическое окружение (Lexical Environment) — внутреннюю структуру, которая хранит переменные и ссылку на внешнее окружение.

function outer() {
  const name = 'Alice';

  function inner() {
    console.log(name); // находит name во внешнем окружении
  }

  inner();
}

outer(); // 'Alice'

Когда функция обращается к переменной, движок ищет её сначала в текущем окружении, затем во внешнем, затем ещё выше — пока не дойдёт до глобального. Эта цепочка поиска называется цепочкой областей видимости (scope chain).

Понимание лексических окружений — ключ к замыканиям. Если функция «уходит» из своего окружения (например, возвращается из другой функции), она всё равно сохраняет ссылку на родительское окружение.


Function declaration vs function expression

Function declaration (объявление функции) полностью поднимается: и имя, и тело доступны до строки объявления:

sayHi(); // 'Hi!'

function sayHi() {
  console.log('Hi!');
}

Function expression (функциональное выражение) поднимается только переменная, а не функция:

sayBye(); // TypeError: sayBye is not a function

var sayBye = function () {
  console.log('Bye!');
};

Если использовать let/const, обращение до объявления вызовет ReferenceError из-за TDZ:

sayBye(); // ReferenceError

const sayBye = function () {
  console.log('Bye!');
};

Hoisting классов

Классы в JavaScript поднимаются, но остаются в TDZ до строки объявления — аналогично let/const:

const dog = new Dog(); // ReferenceError

class Dog {}

Функции-конструкторы, объявленные через function, поднимаются полностью и могут вызываться до объявления. Классы — нет.


Практические примеры: что выведет код

Пример 1

var x = 1;

function foo() {
  console.log(x); // ?
  var x = 2;
  console.log(x); // ?
}

foo();

Ответ: undefined, затем 2. Внутри foo своя переменная x поднимается и затеняет глобальную.

Пример 2

let a = 1;

function bar() {
  console.log(a); // ?
  let a = 2;
}

bar();

Ответ: ReferenceError. Внутри bar есть своя a в TDZ; даже обращение к одноимённой внешней переменной до инициализации запрещено.

Пример 3

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 3, 3, 3

for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j), 0);
}
// 0, 1, 2

var создаёт одну переменную на всю функцию, а let — новую переменную на каждую итерацию цикла. Это классическая задача на замыкания и область видимости.


Ключевые тезисы для интервью

  • var имеет функциональную область видимости и поднимается со значением undefined, а let и const имеют блочную область видимости и попадают в TDZ до инициализации.
  • let позволяет переприсваивать значение, а const фиксирует ссылку — сам объект можно изменять, но переменную перезаписать нельзя.
  • Hoisting — результат двухфазной компиляции JavaScript: на фазе подготовки движок обрабатывает объявления, на фазе выполнения — исполняет код.
  • Function declaration полностью поднимается (и имя, и тело), а function expression поднимает только переменную без значения функции.
  • Классы поднимаются, но остаются в TDZ до строки объявления — аналогично let/const, в отличие от функций-конструкторов через function.
  • Лексическое окружение хранит переменные блока и ссылку на внешнее окружение, а цепочка областей видимости определяет порядок поиска переменных при обращении к ним.
  • var в цикле с setTimeout создаёт одну переменную на все итерации, поэтому callback получает одно значение, а let создаёт новую переменную на каждую итерацию.

Заключение

Область видимости и hoisting — фундаментальные механизмы JavaScript, объясняющие поведение переменных до их объявления и доступность в разных частях кода. Понимание разницы между var, let и const, Temporal Dead Zone и лексическим окружением необходимо для работы с замыканиями и написания предсказуемого кода. Современные рекомендации сводятся к использованию let/const вместо var и function declaration вместо function expression. Попробуйте разобрать несколько примеров «что выведет код» — это лучший способ закрепить материал.

Полезные ссылки