Пошук уроків, статей та іншого контенту
Дізнайтеся, як стек викликів зберігає активні контексти виконання та визначає порядок виконання функцій. Розберіть вкладені виклики, повернення з функцій і причину помилки Maximum call stack size exceeded.
Стек викликів (call stack) — це структура даних, яку JavaScript-двигун використовує для відстеження активних викликів функцій.
Стек працює за принципом LIFO (Last In, First Out):
останній доданий елемент буде видалений першим;
новий виклик функції додається на вершину стека;
після завершення функції її контекст забирається зі стека;
виконання продовжується з того місця, де функцію було викликано.
Кожен елемент стека представляє контекст виконання функції. У ньому зберігаються, зокрема:
параметри функції;
локальні змінні;
поточна позиція виконання;
інформація про те, куди потрібно повернутися після завершення функції.
function first() {
console.log("Початок first");
second();
console.log("Кінець first");
}
function second() {
console.log("Початок second");
third();
console.log("Кінець second");
}
function third() {
console.log("Виконується third");
}
first();Порядок виконання:
JavaScript починає виконувати глобальний код.
Викликається first().
Контекст first додається до стека.
Усередині first викликається second().
Контекст second додається поверх first.
Усередині second викликається third().
Контекст third додається поверх second.
third завершується і забирається зі стека.
Виконання повертається до second.
second завершується і забирається зі стека.
Виконання повертається до first.
first завершується.
У консолі буде:
Початок first
Початок second
Виконується third
Кінець second
Кінець firstУ спрощеному вигляді стек змінюється так:
Глобальний контекстПісля виклику first:
first
Глобальний контекстПісля виклику second:
second
first
Глобальний контекстПісля виклику third:
third
second
first
Глобальний контекстПісля завершення third:
second
first
Глобальний контекстJavaScript виконує синхронний код послідовно. Якщо функція викликає іншу функцію, поточна функція не завершується одразу. Її контекст залишається в стеку, поки вкладений виклик не завершиться.
function printMessage(message) {
console.log(`Повідомлення: ${message}`);
}
function processData() {
const message = "Дані оброблено";
printMessage(message);
console.log("Обробка завершена");
}
processData();
console.log("Програма завершена");Порядок:
Викликається processData.
Створюється локальна змінна message.
Викликається printMessage.
printMessage виводить повідомлення і завершується.
processData продовжує виконання.
processData завершується.
Виконується останній console.log.
Вивід:
Повідомлення: Дані оброблено
Обробка завершена
Програма завершенаВиклик printMessage не запускається «паралельно». Він повністю завершується до того, як processData перейде до наступного рядка.
Коли функція завершується:
її контекст видаляється зі стека;
значення, передане через return, повертається в місце виклику;
виконання продовжується з наступного рядка після виклику.
function multiply(a, b) {
return a * b;
}
function calculate() {
const result = multiply(4, 5);
console.log(result);
}
calculate();Послідовність:
calculate() додається до стека.
Усередині неї викликається multiply(4, 5).
multiply додається до стека.
Функція повертає 20.
Контекст multiply видаляється зі стека.
Значення 20 записується в result.
calculate виводить результат і завершується.
Якщо функція не має return, вона неявно повертає undefined.
function showMessage() {
console.log("Привіт");
}
const result = showMessage();
console.log(result);Вивід:
Привіт
undefinedПід час виконання JavaScript працює з різними контекстами виконання. Найважливіші з них:
глобальний контекст — створюється для основного коду програми;
контекст функції — створюється кожного разу під час виклику функції;
контекст виконання модуля — використовується для коду JavaScript-модуля.
Стек викликів зберігає активні контексти в порядку їхнього вкладення.
function outer() {
const outerValue = "outer";
function inner() {
const innerValue = "inner";
console.log(outerValue);
console.log(innerValue);
}
inner();
}
outer();Коли виконується inner, у стеку одночасно існують контексти:
inner
outer
Глобальний контекстКонтекст inner може звертатися до outerValue, тому що функції також використовують лексичне оточення. Однак стек викликів і лексичне оточення — це різні поняття:
стек визначає, які виклики зараз активні;
лексичне оточення визначає, які змінні доступні функції за місцем її оголошення.
Вкладені виклики можуть утворювати кілька рівнів стека.
function levelOne() {
console.log("Рівень 1");
levelTwo();
console.log("Повернулися на рівень 1");
}
function levelTwo() {
console.log("Рівень 2");
levelThree();
console.log("Повернулися на рівень 2");
}
function levelThree() {
console.log("Рівень 3");
}
levelOne();Після виклику levelThree стек має приблизно такий вигляд:
levelThree
levelTwo
levelOne
Глобальний контекстСпочатку завершиться levelThree, потім levelTwo, і лише після цього levelOne.
Саме тому результат такий:
Рівень 1
Рівень 2
Рівень 3
Повернулися на рівень 2
Повернулися на рівень 1Рекурсія — це ситуація, коли функція викликає саму себе.
Коректна рекурсивна функція повинна мати базовий випадок — умову, за якої рекурсивні виклики припиняються.
function countdown(number) {
if (number === 0) {
console.log("Старт!");
return;
}
console.log(number);
countdown(number - 1);
}
countdown(3);Під час виконання:
countdown(3)
countdown(2)
countdown(1)
countdown(0)Після досягнення базового випадку виклики завершуються у зворотному порядку.
У цій програмі функція викликає себе чотири рази, але кожен виклик завершується:
countdown(0) повертає керування до countdown(1);
countdown(1) завершується;
керування повертається до countdown(2);
countdown(2) завершується;
керування повертається до countdown(3).
Maximum call stack size exceededЯкщо функція викликає себе без умови завершення, кількість активних контекстів постійно збільшується:
function infiniteRecursion() {
infiniteRecursion();
}
infiniteRecursion();У стеку з'являтимуться нові виклики:
infiniteRecursion
infiniteRecursion
infiniteRecursion
...Стек має обмежений розмір. Коли він переповнюється, JavaScript припиняє виконання і викидає помилку:
RangeError: Maximum call stack size exceededТочна кількість дозволених викликів залежить від JavaScript-двигуна та середовища виконання. Не варто покладатися на конкретне число.
Найчастіше потрібно додати базовий випадок:
function printNumbers(number) {
if (number < 1) {
return;
}
console.log(number);
printNumbers(number - 1);
}
printNumbers(3);Також рекурсію можна замінити циклом:
function printNumbers(number) {
for (let current = number; current >= 1; current -= 1) {
console.log(current);
}
}
printNumbers(3);Цикл не створює нового контексту функції для кожної ітерації, тому для великої кількості повторень він часто безпечніший щодо переповнення стека.
Коли виникає помилка, середовище виконання часто показує stack trace — ланцюжок викликів, які привели до помилки.
function parseValue() {
throw new Error("Некоректне значення");
}
function readData() {
parseValue();
}
function start() {
readData();
}
start();У стеку під час створення помилки будуть приблизно такі активні виклики:
parseValue
readData
start
глобальний контекстПовідомлення про помилку може містити назви функцій і номери рядків. Це допомагає визначити:
де виникла помилка;
яка функція її спричинила;
через які виклики програма до неї дійшла.
Для перегляду поточного стека можна створити помилку:
function showStack() {
console.log(new Error("Поточний стек").stack);
}
function run() {
showStack();
}
run();Формат властивості stack залежить від середовища, тому не слід покладатися на однаковий вигляд цього тексту в усіх браузерах і середовищах.
Стек викликів обробляє синхронний код. Асинхронні операції, такі як таймери, не залишають свою функцію в стеку на час очікування.
console.log("Початок");
setTimeout(() => {
console.log("Таймер");
}, 0);
console.log("Кінець");Результат:
Початок
Кінець
ТаймерПояснення:
Виводиться "Початок".
setTimeout реєструє callback для майбутнього виконання.
Виклик setTimeout завершується.
Виводиться "Кінець".
Після завершення поточного синхронного коду callback таймера потрапляє до стека і виконується.
Отже, навіть таймер із затримкою 0 не виконується негайно. Він буде виконаний після очищення стека від поточного синхронного коду.
Важливо розрізняти:
call stack — виконує поточний синхронний код;
черга завдань — зберігає готові асинхронні callback-функції;
event loop — контролює, коли callback можна додати до стека.
Розглянемо приклад:
function add(a, b) {
console.log("add");
return a + b;
}
function calculate() {
console.log("calculate");
const firstResult = add(2, 3);
const secondResult = add(firstResult, 4);
return secondResult;
}
const result = calculate();
console.log(result);Порядок викликів:
Глобальний код викликає calculate.
calculate виводить "calculate".
Викликається перший add.
add повертає 5.
Викликається другий add.
Другий add повертає 9.
calculate повертає 9.
Глобальний код виводить 9.
Результат:
calculate
add
add
9Щоб проаналізувати подібний код, корисно поставити такі запитання:
Яка функція викликається першою?
Які функції викликаються всередині неї?
Коли кожна функція завершується?
Яке значення вона повертає?
Який контекст буде на вершині стека в кожен момент?
Стек працює за принципом LIFO, а черга — за принципом FIFO. Асинхронні callback-функції спочатку очікують у черзі, а не виконуються безпосередньо зі стека.
setTimeout(..., 0) виконується негайноНульова затримка означає, що callback може бути поставлений у чергу після завершення поточного коду. Він не перериває синхронне виконання.
Рекурсія без умови завершення зрештою спричинить:
Maximum call stack size exceededУ межах одного JavaScript-потоку синхронний код виконується послідовно. Наступний виклик не починається, поки поточний не завершиться або не передасть керування асинхронному механізму.
Стек містить активні контексти виконання, але не є загальним сховищем усіх даних. Об'єкти, наприклад, зазвичай розміщуються в купі (heap), а контексти можуть містити посилання на них.
Стек викликів зберігає активні контексти виконання.
Він працює за принципом LIFO: останній виклик завершується першим.
Виклик функції додає її контекст на вершину стека.
Після завершення функції її контекст видаляється зі стека.
return передає значення назад у місце виклику.
Вкладені функції формують кілька рівнів стека.
Рекурсія повинна мати базовий випадок.
Надмірна або нескінченна рекурсія спричиняє Maximum call stack size exceeded.
Асинхронні callback-функції виконуються після завершення поточного синхронного коду.
Аналіз стека допомагає зрозуміти порядок виконання та знайти причину помилок.