Область видимости и hoisting
Область видимости (scope) определяет, где переменная доступна, а hoisting — как движок обрабатывает объявления до выполнения кода. Эти механизмы объясняют множество «странных» примеров на собеседованиях и лежат в основе замыканий.
Содержание
- Виды области видимости
var,let,const- Hoisting и Temporal Dead Zone
- Лексическое окружение
- Function declaration vs function expression
- Hoisting классов
- Практические примеры: что выведет код
- Ключевые тезисы для интервью
- Заключение
- Полезные ссылки
Виды области видимости
В JavaScript есть три уровня области видимости:
- Глобальная — переменная объявлена вне любой функции или блока. Доступна везде (в браузере становится свойством
window, в Node.js —global). - Функциональная — переменная доступна внутри функции, где объявлена, включая вложенные функции.
- Блочная — переменная доступна только внутри блока
{}, где объявлена: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:
- Фаза компиляции/подготовки. Движок проходит по коду, находит все объявления переменных и функций, создаёт лексические окружения.
- Фаза выполнения. Движок выполняет код, используя уже созданные окружения.
Поэтому 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. Попробуйте разобрать несколько примеров «что выведет код» — это лучший способ закрепить материал.