Хойстинг и зона смерти в JavaScript
Продолжаю серию про базовые вопросы для подготовки к собесам. После event loop и замыканий следующая тема — хойстинг. Спрашивают реже, но ответ «переменные поднимаются» слишком слабый. Разберём, как оно работает под капотом.
Что такое хойстинг
JavaScript выполняет код в два этапа. Сначала проходит по всему коду в файле и регистрирует объявления: переменные, функции, классы. Потом выполняет код строка за строкой.
Хойстинг — следствие этой двухэтапной работы движка. Это поведение, при котором объявления оказываются в памяти раньше, чем выполнение до них доходит.
Вроде бы всё просто, но есть нюанс: не все объявления попадают в память одинаково.
function declaration
Объявление функции записывается в память целиком, вместе с телом. Функцию можно вызвать до её объявления в коде.
greet("Вася"); // 'Привет, Вася' — работает
function greet(name) {
console.log(`Привет, ${name}`);
}
В фазе создания движок нашёл function greet и сразу записал её в память. К моменту вызова greet('Вася') функция уже была доступна, хотя в коде объявлена ниже.
Та же механика работает для асинхронных функций async function и генераторов function*: если объявление начинается с ключевого слова function, оно хойстится целиком.
var
В случае с var немного другое поведение: в память попадает только объявление переменной со значением undefined. Присвоение значения происходит позже, в момент выполнения кода.
console.log(name); // undefined — не ошибка
var name = "Вася";
console.log(name); // 'Вася'
В фазе создания движок увидел var name и зарезервировал место в памяти со значением undefined. Присвоение 'Вася' происходит, когда выполнение доходит до этой строки.
let и const: TDZ
let и const тоже регистрируются в фазе создания, но не инициализируются — у них нет даже значения undefined. Они существуют в памяти, но обратиться к ним нельзя — любая попытка до строки с объявлением бросит ReferenceError.
Этот промежуток называется Temporal Dead Zone, TDZ.
console.log(age); // ReferenceError: Cannot access 'age' before initialization
let age = 25;
console.log(typeof age); // ReferenceError — даже typeof не спасает внутри TDZ
let age = 25;
В отличие от var, который молча возвращает undefined, обращение к let/const до объявления бросит ReferenceError — поведение более строгое и предсказуемое, меньше шансов упустить баг.
function expression и стрелочные функции
Если функция присвоена переменной, хойстинг работает по правилам этой переменной, а не по правилам функции.
greet(); // TypeError: greet is not a function
var greet = function () {
console.log("Привет");
};
В фазе создания в память попало только var greet со значением undefined. До строки с присвоением greet это просто undefined, а не функция. Вызов undefined() даёт TypeError.
greet(); // ReferenceError: Cannot access 'greet' before initialization
const greet = () => {
console.log("Привет");
};
В этом примере стрелочная функция объявлена через const, она попадает в TDZ — попытка вызвать её до объявления бросит ReferenceError.
class
Объявление класса движок обрабатывает как let/const: регистрирует в фазе создания, но не инициализирует — класс тоже попадает в TDZ.
const user = new User("Вася"); // ReferenceError
class User {
constructor(name) {
this.name = name;
}
}
В отличие от function declaration, использовать класс до объявления нельзя.
Классический вопрос с собеса
Что выведет этот код?
console.log(typeof foo);
console.log(typeof bar);
var foo = 1;
function bar() {}
Без знания того, как работает хойстинг, ожидания могут быть разными: ошибка или undefined для обоих. На самом деле:
typeof foo—'undefined':var fooзарегистрировался в фазе создания со значениемundefined,typeof undefinedвозвращает строку'undefined'typeof bar—'function':function barзаписалась в память как function declaration — вместе с телом
А теперь усложнённый вариант:
var foo = 1;
function foo() {
return 2;
}
console.log(typeof foo); // 'number'
В фазе создания function foo записывается в память первой. Затем выполнение доходит до var foo = 1 и перезаписывает значение числом. Объявление var foo игнорируется — имя уже зарезервировано функцией, но присвоение выполняется.
Почему это спрашивают на собеседовании
Хойстинг — это не самостоятельная фича языка, а скорее побочный эффект того, как движок читает код.
Несколько ситуаций, где незнание хойстинга приводит к проблемам:
- Использование
varдо объявления. Переменная уже существует в памяти со значениемundefined, хотя присвоение ещё не случилось. Код не падает с ошибкой, а молча работает не так, как ожидалось. - Временная мёртвая зона с
letиconst. Переменные уже зарегистрированы, но обратиться к ним нельзя — до строки с объявлением броситReferenceError. Выглядит неожиданно, если не знать про TDZ. - Разница между
function declarationиfunction expression. Первая хойстится целиком и доступна до объявления. Вторая — нет. Выглядят похоже, ведут себя по-разному. - Смешивание
function declarationиvarс одним именем. Порядок выполнения не совпадает с порядком в коде, и результат зависит от того, что именно и когда попало в память.
На собеседовании эта тема позволяет понять, знаете ли вы, как движок выполняет код — и как не выстрелить себе в ногу из-за его особенностей.
← К статьям