Пошук уроків, статей та іншого контенту
Що рушій робить під час запуску кожної функції, і чому порядок виконання коду не завжди очевидний з першого погляду.
Дотепер ми розглядали JavaScript як «чорну скриньку»: написали код — він виконався зверху вниз. Це працює для простих прикладів, але не пояснює, чому hoisting піднімає одні оголошення, а не інші, чому рекурсія (модуль «Функції») зрештою кидає RangeError, чи як this визначає, до якого об'єкта він належить. Відповідь на всі ці питання — у тому, як рушій керує виконанням коду через контексти виконання та стек викликів.
Контекст виконання — це «середовище», у якому виконується частина коду: воно містить усі змінні, оголошені в цій області видимості, значення this та посилання на зовнішній контекст (для ланцюжка областей видимості з модуля «Функції»). Є два основні типи:
Глобальний контекст виконання — створюється один раз, коли починається виконання скрипту. this на верхньому рівні модуля дорівнює undefined (у strict mode) або глобальному об'єкту.
Контекст виконання функції — створюється заново щоразу, коли функцію викликають, і знищується (стає доступним для garbage collection), коли вона завершує роботу — якщо тільки замикання (модуль «Функції») не утримує посилання на нього.
Кожного разу, коли викликається функція, для неї створюється новий контекст виконання, і він додається («push») на вершину стека викликів. Коли функція завершує роботу (return або кінець тіла), її контекст знімається («pop») зі стека, і виконання повертається до того, хто був під ним:
function first() {
console.log("Початок first");
second();
console.log("Кінець first");
}
function second() {
console.log("У second");
}
first();
// Початок first
// У second
// Кінець firstПростежимо стек крок за кроком для цього прикладу:
Викликається first() → стек: [global, first]
first виводить "Початок first", потім викликає second() → стек: [global, first, second]
second виводить "У second" і завершується → стек знову: [global, first]
first виводить "Кінець first" і завершується → стек: [global]
Стек викликів має обмежений розмір у пам'яті. Кожен рекурсивний виклик без досягнутого базового випадку (модуль «Функції») додає новий контекст на стек, поки він не переповниться — саме тому глибока рекурсія без умови зупинки кидає RangeError: Maximum call stack size exceeded, а не виконується вічно.
JavaScript однопотоковий (модуль «Асинхронний JavaScript»): активним може бути лише один контекст виконання водночас — той, що на вершині стека. Це саме те, що робить порядок виконання прогнозованим: наступний рядок коду не почне виконуватись, поки поточна функція (і все, що вона викликала) не завершиться.
Коли в коді виникає необроблена помилка, повідомлення в консолі показує саме стек викликів на момент помилки — це і є практична причина, чому корисно розуміти цю механіку:
function validateAge(age) {
if (age < 0) throw new Error("Вік не може бути від'ємним");
return age;
}
function createUser(name, age) {
return { name, age: validateAge(age) };
}
createUser("Олена", -5);
// Error: Вік не може бути від'ємним
// at validateAge (...)
// at createUser (...) ← стек показує весь шлях виклику
// at ...Очікування, що дві функції виконуються «одночасно», якщо одна викликає іншу — насправді виконання завжди послідовне, поки не задіяно асинхронний код.
Плутанина глибокої рекурсії з нескінченним циклом — вони дають різні помилки (RangeError для стека, зависання для циклу без зупинки).
Ігнорування стека помилки при дебазі — саме він показує точний ланцюжок викликів, що призвів до проблеми.
Кожен виклик функції створює новий контекст виконання й додає його на стек викликів; завершення функції знімає контекст зі стека. Це механізм, який лежить в основі синхронного, передбачуваного порядку виконання коду, обмеження на глибину рекурсії та інформації, яку показує трасування помилки.
Спробуйте самостійно
Напишіть три функції a, b, c, де a викликає b, а b викликає c. У кожній виведіть повідомлення до і після виклику наступної функції, запишіть очікуваний порядок виводу на папері, а потім перевірте в консолі.