Skip to main content

Підняття в JavaScript

Hoisting (підняття) - це поведінка JavaScript, коли оголошення змінних і функцій реєструються в пам'яті під час компіляції, ще до виконання першого рядка коду.

Теорія

TL;DR

  • Уяви перекличку перед уроком: вчитель відмічає всіх присутніх до початку заняття. Оголошення зафіксовані, а значення прийдуть пізніше.
  • Оголошення функцій через function підіймаються повністю. Їх можна викликати до того, як вони з'являться в коді.
  • var підіймається та ініціалізується значенням undefined. Звернення до нього до рядка з присвоєнням поверне undefined, а не помилку.
  • let і const підіймаються, але не ініціалізуються. Звернення до них до оголошення кидає ReferenceError. Цей проміжок називається temporal dead zone (TDZ, тимчасова мертва зона).
  • Правило вибору: const за замовчуванням, let якщо значення змінюватиметься, var тільки в легасі-коді.

Швидкий приклад

javascript
// Оголошення функції - підняте повністю, це працює 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 поважає межі блоку

javascript
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: Рефакторинг оголошення функції в стрілкову

javascript
// Працює 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: Баг із замиканням у циклі

javascript
// Друкує 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: Вважати що функціональний вираз підіймається

javascript
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 гарантує що змінна зайнята своїм блоком ще до того, як стане доступною.

Приклади

Підняття оголошення функції в модулі

javascript
// Виклик до визначення - працює завдяки 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.

Баг із замиканням у циклі та виправлення

javascript
// Класична пастка на співбесіді 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 створює нове прив'язування на кожній ітерації.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?