Підняття в JavaScript
Hoisting (підняття) - це поведінка JavaScript, коли оголошення змінних і функцій реєструються в пам'яті під час компіляції, ще до виконання першого рядка коду.
Теорія
TL;DR
- Уяви перекличку перед уроком: вчитель відмічає всіх присутніх до початку заняття. Оголошення зафіксовані, а значення прийдуть пізніше.
- Оголошення функцій через
functionпідіймаються повністю. Їх можна викликати до того, як вони з'являться в коді. varпідіймається та ініціалізується значеннямundefined. Звернення до нього до рядка з присвоєнням повернеundefined, а не помилку.letіconstпідіймаються, але не ініціалізуються. Звернення до них до оголошення кидаєReferenceError. Цей проміжок називається temporal dead zone (TDZ, тимчасова мертва зона).- Правило вибору:
constза замовчуванням,letякщо значення змінюватиметься,varтільки в легасі-коді.
Швидкий приклад
// Оголошення функції - підняте повністю, це працює
console.log(add(2, 3)); // 5
function add(a, b) {
return a + b;
}
// var - піднято як undefined
console.log(name); // undefined (не помилка)
var name = "Alice";
// let/const - temporal dead zone
console.log(age); // ReferenceError: Cannot access 'age' before initialization
let age = 25;Рушій вже знає про add, name і age до першого рядка. Але тільки add має своє повне значення в той момент.
Головна різниця
JavaScript-рушій обходить твій код двічі. Перший прохід (компіляція): сканує всі оголошення, реєструє їх у пам'яті. Оголошення функцій отримують повний об'єкт функції одразу. Змінні var отримують undefined. let і const реєструються, але залишаються неініціалізованими. Саме тому звернення до них до рядка з оголошенням кидає помилку замість тихого повернення undefined. TDZ існує навмисно, щоб ловити баги.
Коли що використовувати
- Оголошення функцій: Коли треба викликати допоміжну функцію на початку файлу, а визначити її нижче. Поширено в Express-роутах і Node.js-модулях.
var: Уникай у сучасному коді. Виняток - підтримка IE8 або старіших середовищ.let/const: За замовчуванням для всього нового коду.constзапобігає випадковому переприсвоєнню,letсигналізує що значення змінюватиметься.
Два проходи рушія
V8 (Chrome/Node.js) і SpiderMonkey (Firefox) обидва роблять два проходи. Перший: сканує область видимості, виділяє пам'ять, встановлює початкові значення. Другий: виконує рядок за рядком. Для var і оголошень функцій перший прохід встановлює реальне початкове значення. Для let/const перший прохід тільки реєструє ім'я. Значення з'являється в другому проході, на точному рядку оголошення.
Типові помилки
Помилка 1: Очікувати що var поважає межі блоку
function test() {
console.log(y); // undefined, не помилка
if (true) {
var y = 10;
}
console.log(y); // 10 - y існує в області функції, не блоку
}
test();var ігнорує межі блоків. y підіймається до функціональної області видимості, тому обидва виклики console.log його бачать. Перший повертає undefined, другий - 10. Заміна на let змусила б перший рядок кинути ReferenceError, що зазвичай і є бажаною поведінкою.
Помилка 2: Рефакторинг оголошення функції в стрілкову
// Працює
processData(rawData);
function processData(data) {
return data.map(x => x * 2);
}
// Після рефакторингу - ламається
processData(rawData); // TypeError: processData is not a function
const processData = (data) => data.map(x => x * 2);Стрілкові функції та функціональні вирази не підіймаються повністю. Якщо хтось конвертує function у const, всі виклики вище по файлу одразу ламаються. Класична пастка під час рефакторингу, яка легко проходить локальні тести але ламає білд у іншому порядку файлів.
Помилка 3: Баг із замиканням у циклі
// Друкує 3, 3, 3
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Друкує 0, 1, 2
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}var i підіймається до функціональної області видимості. Всі три колбеки посилаються на одну й ту ж i, яка вже дорівнює 3 на момент виконання. let i створює окреме прив'язування для кожної ітерації.
Помилка 4: Вважати що функціональний вираз підіймається
foo(); // TypeError: foo is not a function
var foo = function() { return 42; };var foo підіймається як undefined. Тіло функції - ні. Виклик foo() до присвоєння - це виклик undefined().
Де зустрічається у реальному коді
- Express.js: Обробники роутів як оголошення функцій можуть посилатись на middleware, визначений нижче в тому ж файлі.
- Node.js-модулі: Публічні функції зверху, приватні хелпери знизу. Працює бо оголошення функцій підняті в межах модуля.
- Jest/Mocha:
describe()блоки можуть посилатись на допоміжні функції тестів, визначені після них. - React-класи: Методи можуть викликати інші методи класу незалежно від порядку їх визначення в тілі класу.
Питання на співбесіді
Q: Що таке temporal dead zone?
A: TDZ - це проміжок між входом у область видимості та рядком з оголошенням let/const. В цей час змінна зареєстрована, але не ініціалізована. Звернення до неї кидає ReferenceError. Відрізняється від var, де змінна одразу ініціалізується як undefined.
Q: Чому console.log(x) виводить undefined, а не кидає помилку, якщо var x оголошено нижче?
A: Тому що var x підіймається і відразу ініціалізується значенням undefined. Рушій бачить це як var x = undefined; console.log(x); x = 5;. Присвоєння залишається на своєму рядку, але оголошення з початковим значенням переміщується вгору.
Q: Чи підіймаються функціональні вирази і стрілкові функції?
A: Ні. Тільки оголошення функцій підіймаються разом із тілом. Функціональні вирази в var підіймаються як undefined. У let/const вони потрапляють у TDZ. Виклик до оголошення в обох випадках кидає помилку.
Q (Senior): let і const підіймаються, але не ініціалізуються. Навіщо взагалі їх підіймати?
A: Підняття визначає якій області видимості належить змінна. Якщо let x є в блоці, JavaScript знає що x належить цьому блоку, а не зовнішній області. Без реєстрації в першому проході рушій пішов би по ланцюгу областей видимості і міг знайти зовнішній x, повернувши значення замість помилки. TDZ гарантує що змінна зайнята своїм блоком ще до того, як стане доступною.
Приклади
Підняття оголошення функції в модулі
// Виклик до визначення - працює завдяки hoisting
const logger = createLogger("APP");
logger("Server started"); // [APP] Server started
function createLogger(prefix) {
return function(message) {
logMessage(message);
};
// logMessage підняте в межах createLogger
function logMessage(msg) {
console.log(`[${prefix}] ${msg}`);
}
}createLogger підняте до рівня модуля. Всередині нього logMessage підняте до області createLogger. Повернута функція може безпечно викликати logMessage, навіть якщо воно визначено після return.
Баг із замиканням у циклі та виправлення
// Класична пастка на співбесіді
function withVar() {
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
}
withVar(); // 3, 3, 3 - i спільна для всіх ітерацій
// Виправлення через let
function withLet() {
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
}
withLet(); // 0, 1, 2 - кожна ітерація має свою iОдне з найпоширеніших hoisting-питань на JavaScript-співбесідах. var підіймає i до функціональної області, і всі три замикання захоплюють одну й ту ж змінну. Коли колбеки виконуються, цикл вже завершено і i дорівнює 3. let створює нове прив'язування на кожній ітерації.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.