Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке call stack в JavaScript?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Call stack** (стек викликів) відстежує яка функція виконується у JavaScript, за принципом LIFO: останній зайшов, перший вийшов. ```javascript function a() { b(); } function b() { console.log('b'); } a(); // стек: [a, b] -> [a] -> [] ``` **Ключове:** один call stack - одночасно виконується лише одне.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Call stack** (стек викликів) - це структура даних, яку JavaScript використовує для відстеження того, яка функція зараз виконується і куди повернутись після її завершення. ## Теорія ### TL;DR - Call stack працює за принципом LIFO (Last In, First Out): остання викликана функція завершується першою - Кожен виклик функції створює **stack frame** (фрейм стеку) із локальними змінними, `this` і адресою повернення - У JavaScript один call stack - одночасно виконується тільки одна операція - Коли стек порожній, event loop може запустити наступну задачу з черги - Нескінченна рекурсія без базової умови переповнює стек і кидає `RangeError: Maximum call stack size exceeded` ### Як працює стек Коли JavaScript викликає функцію, він додає (push) фрейм на стек. Коли функція повертає значення, фрейм знімається (pop). Двигун завжди виконує те, що зверху. ```javascript function greet(name) { return `Hello, ${name}`; } function run() { const message = greet('Alice'); // greet додається, потім знімається console.log(message); // run ще на стеку } run(); // Стек: [run] -> [run, greet] -> [run] -> [] ``` Після виклику `run()` стек виглядає як `[run]`. Коли всередині викликається `greet`, стає `[run, greet]`. Коли `greet` повертає значення - знову `[run]`. Після завершення `run` стек порожній. ### Stack frames Кожен фрейм - це не просто покажчик. Він містить локальні змінні функції, значення `this` і адресу повернення (куди продовжити після виходу). Саме тому глибока рекурсія їсть пам'ять: кожен фрейм займає місце і жоден не звільняється поки функція не повернеться. ### Stack overflow (переповнення стеку) Без базової умови в рекурсивній функції фрейми накопичуються нескінченно. ```javascript function countdown(n) { console.log(n); countdown(n - 1); // ніколи не зупиняється } countdown(10000); // RangeError: Maximum call stack size exceeded ``` V8 (Chrome, Node.js) зазвичай кидає помилку десь після 10 000-12 000 фреймів. Базова умова вирішує проблему: ```javascript function countdown(n) { if (n < 0) return; // базова умова console.log(n); countdown(n - 1); } ``` ### Однопотоковість У JavaScript один call stack. Це архітектурне рішення, а не обмеження. Воно робить код передбачуваним: немає гонки потоків, немає спільного змінного стану, немає замків. Але це означає: одна важка операція блокує все. Цикл `while` на 5 секунд заморожує вкладку, бо стек ніколи не звільняється і інший код не може запуститись. CPU-важкі задачі варто переносити в Web Worker. ### Call stack і event loop [Event loop](/questions/event-loop-in-javascript) стежить за call stack. Коли стек порожній, event loop бере наступний колбек з черги задач і додає його на стек. Асинхронні колбеки, таймери і обробники Promise чекають у черзі і виконуються тільки після звільнення стеку. ```javascript console.log('start'); setTimeout(() => { console.log('timeout'); // додається в чергу, запускається після очищення стеку }, 0); console.log('end'); // Виведення: // start // end // timeout ``` Навіть із затримкою 0мс `setTimeout` виконується після `end`. Колбек чекає в черзі, поки `console.log('end')` ще на стеку. ### Читання stack trace Коли виникає помилка, stack trace показує стек викликів у той момент. Читати знизу вгору - отримаєш порядок викликів. ```javascript function a() { b(); } function b() { c(); } function c() { throw new Error('oops'); } a(); // Error: oops // at c (file.js:3) // at b (file.js:2) // at a (file.js:1) ``` Нижній рядок (`a`) - де почалось виконання. Верхній (`c`) - де впало. Більшість розробників читають тільки перший рядок і зупиняються - це помилка. ### Типові помилки **1. Думати, що async-колбеки виконуються в поточному стеку** ```javascript function fetchData() { fetch('/api/data').then(res => { console.log('inside then'); }); console.log('after fetch'); } fetchData(); // Виведення: // after fetch // inside then <- виконується ПІСЛЯ того як fetchData вже знятий зі стеку ``` Колбек `.then` виконується після того, як `fetchData` вже пішов зі стеку. Він чекав у черзі мікрозадач. **2. Вважати, що `setTimeout(fn, 0)` запускається одразу** Нуль мілісекунд означає "після того як поточний стек порожній", а не "прямо зараз". Якщо стек зайнятий 2 секунди - колбек чекатиме ці 2 секунди плюс 0мс. **3. Блокувати стек синхронними циклами** ```javascript const start = Date.now(); while (Date.now() - start < 3000) {} // busy-wait на 3 секунди console.log('done'); // Жодна подія, клік або таймер не спрацював ці 3 секунди ``` Переноси важку роботу в Web Worker або розбивай на частини через `setTimeout`. **4. Неправильно читати stack trace** Stack trace читається знизу вгору за порядком викликів, але сама помилка - зверху. Читай весь trace, щоб знайти де виклик почався. ### Де зустрічається - **Browser DevTools:** панель "Call Stack" показує живий стек під час покрокового виконання - **React:** функції рендеру компонентів з'являються на стеку під час реконсиляції; помилки показують component stack trace - **Express:** функції middleware виконуються як вкладені виклики; необроблені помилки піднімаються по стеку до error handler - **Node.js:** `Error.captureStackTrace` дозволяє захопити стек у будь-який момент для діагностики ### Питання на співбесіді **Q:** Що відбувається коли call stack порожній? **A:** Event loop перевіряє чергу мікрозадач (Promise) і повністю її очищає. Після цього бере одну задачу з черги макрозадач (setTimeout, setInterval) і додає її на стек. **Q:** Чому довгий цикл `for` блокує UI у браузері? **A:** Цикл тримає call stack зайнятим весь цей час. Event loop не може додати жодний колбек (обробник кліку, запит на перерисовку, таймер) поки стек не звільниться. Рішення - розбити роботу на частини через `setTimeout` або `requestAnimationFrame`. **Q:** Чи використовує `async/await` call stack інакше? **A:** Код до першого `await` виконується на стеку звично. На `await` функція призупиняється і її фрейм знімається зі стеку. Коли Promise вирішується, функція відновлюється через мікрозадачу і новий фрейм додається для коду після `await`. **Q:** Що таке stack frame? **A:** Блок пам'яті, який двигун виділяє для одного виклику функції. Містить локальні змінні, об'єкт `arguments`, прив'язку `this` і адресу повернення. Фрейм звільняється коли функція повертає значення. **Q:** Чи можна збільшити максимальний розмір стеку? **A:** У Node.js так: `node --stack-size=65536 app.js`. У браузерах - ні. Практичне рішення для глибокої рекурсії - переписати ітеративно або використати trampolining. ## Приклади ### Базовий stack trace ```javascript function add(a, b) { return a + b; // стек: [add] } function calculate(x, y) { const result = add(x, y); // стек: [calculate, add] -> [calculate] return result; } function main() { const total = calculate(5, 3); // стек: [main, calculate] -> [main] -> [] console.log(total); // 8 } main(); ``` `main` викликає `calculate`, яка викликає `add`. Кожен виклик додає фрейм. Кожне повернення знімає один. Після завершення `main` стек порожній і виводиться `8`. ### Обробка замовлення з відстеженням помилок ```javascript function processOrder(order) { validateOrder(order); const total = calcTotal(order); notifyUser(order, total); } function validateOrder(order) { if (!order.items || order.items.length === 0) { throw new Error('Order has no items'); // Стек в цей момент: [validateOrder, processOrder] } } function calcTotal(order) { return order.items.reduce((sum, item) => sum + item.price, 0); } function notifyUser(order, total) { console.log(`Замовлення ${order.id} підтверджено. Сума: $${total}`); } processOrder({ id: 42, items: [{ price: 19.99 }, { price: 5.50 }], email: 'user@example.com' }); // Замовлення 42 підтверджено. Сума: $25.49 ``` Якщо `validateOrder` кидає помилку, вона піднімається через `processOrder`. Stack trace покаже обидва фрейми, що полегшує пошук проблеми. ### Асинхронний код і стек ```javascript async function getUserData(userId) { // Стек: [getUserData] поки виконується цей рядок const response = await fetch(`/api/users/${userId}`); // Стек порожній під час очікування - getUserData призупинена // Стек: [getUserData] відновлюється тут коли Promise вирішується return response.json(); } async function renderProfile(userId) { const user = await getUserData(userId); console.log(`Рендеримо профіль для ${user.name}`); } renderProfile(1); // Стек не заблокований під час fetch - інший код може виконуватись ``` Під час `fetch` стек не заблокований. Функція призупиняється, звільняючи стек для іншої роботи. В цьому і є сенс [async/await](/questions/async-await-in-javascript).Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.