Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Контекст виконання в JavaScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Контекст виконання** (execution context) в JavaScript - це середовище, яке рушій створює для запуску фрагменту коду. У ньому зберігаються змінні, прив'язка `this` і посилання на зовнішній scope. ```javascript var x = 1; // глобальний execution context function fn() { let y = 2; // власний execution context функції fn } ``` **Ключове:** кожен виклик функції створює новий контекст із двома фазами: створення (hoisting, виділення пам'яті) і виконання (код рядок за рядком). Call stack відстежує всі активні контексти за принципом LIFO.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Контекст виконання** (execution context) - це середовище, яке JavaScript створює для кожного фрагменту коду. У ньому зберігаються змінні, прив'язка `this` і посилання на зовнішній scope. ## Теорія ### TL;DR - Уяви робочий стіл: кожен виклик функції отримує свій стіл (контекст) із власними нотатками (змінними), знає хто керівник (outer scope) і яке поточне завдання (`this`). - Три типи: глобальний (один на скрипт), функціональний (один на виклик), eval (всередині `eval()`). - Дві фази: фаза створення виділяє пам'ять, фаза виконання запускає код. - `var` піднімається до `undefined` у фазі створення; `let`/`const` залишаються неініціалізованими (Temporal Dead Zone). - Контексти стекуються в call stack. Внутрішні контексти мають доступ до зовнішніх через scope chain. Назад - ні. ### Швидкий приклад ```javascript console.log(x); // undefined - var піднятий у фазі створення var x = 1; console.log(x); // 1 function test() { console.log(y); // ReferenceError - let залишається в TDZ let y = 2; } test(); ``` `var x` виділяється і отримує значення `undefined` до того, як виконується будь-який код. `let y` всередині `test()` існує в пам'яті, але до нього не можна звернутись поки не виконається той рядок. Цей проміжок і є Temporal Dead Zone. ### Що всередині execution context Кожен контекст, глобальний чи функціональний, має однакову структуру: - **LexicalEnvironment** зберігає `let`, `const` і оголошення функцій, плюс посилання на зовнішнє середовище (scope chain). - **VariableEnvironment** зберігає `var`-декларації і аргументи функції. - **ThisBinding** тримає те, на що вказує `this` у цьому контексті. У браузері глобальний контекст встановлює `this` як `window`. У Node.js як `globalThis` (або `{}` у модулі). ### Дві фази детальніше **Фаза створення** виконується до першого рядка коду. Рушій сканує scope, виділяє пам'ять і: - встановлює `var`-декларації у `undefined` - позначає `let`/`const` як неініціалізовані (TDZ) - зберігає повні оголошення функцій (не функціональні вирази) одразу **Фаза виконання** запускає код зверху вниз. Змінні отримують реальні значення, викликаються функції, і кожен виклик додає новий контекст до call stack. ### Call stack Call stack - це структура LIFO. Коли викликається функція, новий контекст кладеться зверху. Коли функція повертається, контекст знімається. ```javascript function first() { second(); } function second() { third(); } function third() { console.log("third"); } first(); // Стек у найглибшій точці: // [Global] -> [first()] -> [second()] -> [third()] ``` V8 (Chrome і Node.js) підтримує приблизно 10 000 фреймів, після чого кидає `RangeError: Maximum call stack size exceeded`. Нескінченна рекурсія досягає цього ліміту дуже швидко. ### Ключова різниця: LexicalEnvironment і VariableEnvironment Обидва живуть всередині кожного execution context, але зберігають різне. `let`, `const` і оголошення функцій йдуть у `LexicalEnvironment`. `var` і `arguments` - у `VariableEnvironment`. Практичний наслідок: `var` ініціалізується до `undefined` під час створення, тому його можна зчитати до відповідного рядка. `let`/`const` - ні, звернення раніше кидає помилку. [Замикання (closure)](/questions/closures-in-javascript) працює тому, що `LexicalEnvironment` внутрішньої функції тримає посилання на середовище зовнішнього контексту. Це посилання не зникає після того, як зовнішня функція повернулась. ### Коли застосовувати це знання - Відлагоджуєш `this is undefined` у callback: відтвори execution context подумки, щоб зрозуміти що таке `this` на момент виклику. - Пояснюєш чому `var`-змінна дорівнює `undefined` замість `ReferenceError`: фаза створення, дивись [hoisting](/questions/hoisting-in-javascript). - Розбираєшся зі stale closure у React: хук захопив змінні з контексту попереднього рендеру. - Трейсиш scope вкладених функцій: йди по scope chain вгору через батьківські контексти. ### Як V8 обробляє це всередині V8 спочатку парсить код в Abstract Syntax Tree. Для кожного execution context інтерпретатор Ignition будує записи LexicalEnvironment і VariableEnvironment, потім резолвить `this`. Для async-генераторів Ignition призупиняє контекст на `yield` і відновлює його з тим самим середовищем, не розмотуючи call stack. Так само працює `await`: функціональний контекст призупиняється, управління повертається до [event loop](/questions/event-loop-in-javascript), а черга мікрозадач планує відновлення. ### Типові помилки **Вважати що `let` піднімається як `var`:** ```javascript console.log(name); // ReferenceError, не undefined let name = 'Alice'; ``` `let` піднімається в тому сенсі, що рушій знає про його існування, але не ініціалізує до відповідного рядка. Звернення раніше кидає помилку. Ця помилка часто зустрічається у кодових базах, де `var` і `let` використовуються разом у спадковому коді. **Втрата `this` у вкладеному callback:** ```javascript const obj = { path: '/users', handle: function(req, res) { setTimeout(function() { console.log(this.path); // undefined - новий контекст, this = global }, 0); } }; ``` Стрілкова функція вирішує це. Вона не створює власного `this` і успадковує його від оточуючого контексту. ```javascript setTimeout(() => { console.log(this.path); // '/users' }, 0); ``` **Очікувати `window` як `this` у strict mode:** ```javascript 'use strict'; function fn() { console.log(this); // undefined, не window } fn(); ``` У strict mode `this` функціонального контексту стає `undefined`, коли функція викликається без явного отримувача. **Переповнення стека через рекурсію:** ```javascript function factorial(n) { return n <= 1 ? 1 : n * factorial(n - 1); } factorial(10000); // RangeError у V8 (ліміт ~10k фреймів) ``` Кожен виклик додає новий execution context. Використовуй цикл або trampoline-патерн для глибокої рекурсії. ### Де зустрічається в реальному коді - React: кожен рендер компонента - це виклик функції. Хуки `useState` і `useCallback` працюють тому, що їх замикання зберігають функціональний контекст відповідного рендер-циклу. - Express: кожен route handler виконується у власному функціональному контексті. Стрілкові функції у middleware зберігають `this` з зовнішнього scope. - Node.js ESM: кожен модуль виконується у власному функціональному контексті, що дає приватний scope без забруднення `globalThis`. - Redux: редьюсери отримують доступ до стану через замикання з контексту store, а не через `this`. ### Питання на співбесіді **Q:** Що відбувається у фазі створення? **A:** Рушій виділяє пам'ять для всіх декларацій в scope. `var`-змінні отримують `undefined`, оголошення функцій зберігаються повністю, `let`/`const` позначаються як неініціалізовані. Прив'язується `this` і встановлюється посилання на зовнішнє середовище. **Q:** Як замикання (closure) пов'язане з execution context? **A:** Коли визначається внутрішня функція, її `LexicalEnvironment` зберігає посилання на середовище зовнішньої функції. Це посилання живе навіть після того, як зовнішня функція повернулась. Це і є замикання. **Q:** Що таке Temporal Dead Zone? **A:** Період між початком фази створення і рядком де ініціалізується `let` або `const`. У цей час змінна існує в пам'яті, але звернення до неї кидає `ReferenceError`. **Q:** Яка різниця між глобальним контекстом у браузері та Node.js? **A:** У браузері `this` глобального контексту дорівнює `window`. У Node.js CommonJS-модулях `this` на верхньому рівні - це `{}`, а не `global`. Сам об'єкт `global` доступний, але він не збігається з `this` у модулі. **Q:** Як V8 обробляє async-генератор з точки зору execution context? **A:** Ignition призупиняє execution context генератора на кожному `yield`. Контекст зберігається в пам'яті, а не в call stack, і відновлюється коли генератор знову ітерується. Для `async`-функцій `await` робить те саме: контекст призупиняється, а черга мікрозадач планує відновлення з тим самим середовищем. ## Приклади ### Базовий: підняття (hoisting) у фазі створення ```javascript function outer() { console.log(a); // undefined - var піднятий під час створення var a = 1; function inner() { console.log(a); // 1 - зчитує з контексту outer через scope chain } inner(); } outer(); // Виведе: // undefined // 1 ``` Після виклику `outer` створюється новий функціональний execution context. У фазі створення `a` піднімається до `undefined`. У фазі виконання `a` стає `1`. Коли запускається `inner`, власного `a` у неї немає, тому вона піднімається по scope chain до контексту `outer` і знаходить `a = 1`. ### Середній: stale closure в React effect ```javascript import { useState, useEffect } from 'react'; function Counter() { const [count, setCount] = useState(0); useEffect(() => { const interval = setInterval(() => { // Захоплює 'count' з контексту рендеру, коли запустився effect. // Якщо count був 0 тоді - він залишиться 0 тут, stale closure. console.log(count); // завжди 0, якщо deps array порожній }, 1000); return () => clearInterval(interval); }, []); // порожні deps: запускається один раз, захоплює початковий контекст return <button onClick={() => setCount(c => c + 1)}>{count}</button>; } ``` Callback `setInterval` замикається на `count` з контексту першого рендеру. Той контекст заморожений на `count = 0`. Наступні рендери створюють нові функціональні контексти з новими значеннями, але інтервал їх не бачить. Рішення: додай `count` до масиву залежностей, або використовуй `setCount(c => c + 1)`, щоб взагалі не потрібен `count` у замиканні. ### Просунутий: втрата `this` у Express route handler ```javascript const express = require('express'); const router = express.Router(); router.get('/users', function(req, res) { // Звичайна функція: this = router setTimeout(function() { // Новий execution context: this = global (або undefined у strict mode) console.log(this); // {} або undefined res.json({ ok: true }); }, 0); }); // Рішення: стрілкова функція успадковує this з оточуючого контексту router.get('/users-fixed', function(req, res) { setTimeout(() => { console.log(this); // router - успадкований від контексту route handler res.json({ ok: true }); }, 0); }); ``` Стрілкова функція всередині `setTimeout` не створює власного execution context для `this`. Вона бере `this` з найближчого звичайного функціонального контексту вище, тобто з route handler. Це весь механізм того, чому стрілкові функції вирішують проблему втрати `this` у callback-ах.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.