Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Область видимості в JavaScript: типи та принципи роботи». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Область видимості (scope)** в JavaScript визначає, де змінну можна прочитати. Три типи: глобальна (весь код), функції (всередині функції) і блокова (всередині `{}`). ```javascript if (true) { var x = 1; // область функції, витікає назовні let y = 2; // блокова область, залишається всередині } console.log(x); // 1 console.log(y); // ReferenceError ``` **Головне:** `let`/`const` мають блокову область видимості; `var` має область функції і ігнорує блоки.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Область видимості (scope)** в JavaScript визначає, де змінну можна прочитати або змінити. Рушій вирішує scope під час компіляції, а не виконання. Тому це називають лексичною областю видимості. ## Теорія ### TL;DR - Scope = ділянка коду, де існує змінна і де до неї є доступ - Три типи: глобальна (весь код), функції (всередині однієї функції), блокова (всередині одного `{}`) - `var` враховує тільки межі функції; `let` і `const` враховують межі блоку - Вкладені scope-и бачать змінні батьківських, але не навпаки - Правило вибору: оголошуй `let`/`const` у найменшому можливому блоці, уникай `var` ### Швидкий приклад ```javascript const globalVar = 'global'; // глобальна область видимості function outer() { const outerVar = 'outer'; // область функції if (true) { let blockVar = 'block'; // блокова область console.log(globalVar); // "global" - читає вгору по ланцюжку console.log(outerVar); // "outer" - читає вгору по ланцюжку console.log(blockVar); // "block" - локальна } console.log(blockVar); // ReferenceError - блок завершився } outer(); console.log(outerVar); // ReferenceError - функція завершилась ``` Кожна scope може читати змінні з батьківської, але батьківська не дістається до дочірньої. ### Ланцюжок областей видимості JavaScript використовує **лексичну область видимості**: вона визначається тим, де написаний код, а не де він запускається. Коли рушій шукає змінну, він починає з поточної scope і піднімається вгору по ланцюжку до глобальної, поки не знайде змінну або не кине `ReferenceError`. V8 створює об'єкт `LexicalEnvironment` для кожної scope під час компіляції. Ці об'єкти пов'язані з батьківськими через зовнішнє посилання і утворюють ланцюжок. Пошук змінних під час виконання іде по цих посиланнях крок за кроком. ### Коли що використовувати - Глобальна scope: константи для всього застосунку, наприклад `process.env.NODE_ENV` або спільний конфіг - Scope функції: ізолюй логіку всередині утиліти або обробника, щоб вона не впливала на інші функції - Блокова scope: обмеж змінну циклом `for`, блоком `if` або `try/catch` без витоку назовні Чим вужча область видимості, тим менше несподіванок під час code review. ### var, let і const `var` прив'язується до найближчої функції (або глобальної scope, якщо поза всіма функціями). Блоки вона ігнорує повністю. `let` і `const` прив'язуються до найближчого блоку. Вони також підпадають під **Temporal Dead Zone (TDZ)**: змінна існує в scope з початку блоку, але будь-який доступ до рядка оголошення кидає `ReferenceError`. ```javascript console.log(typeof a); // "undefined" - var підняте, ініціалізоване undefined console.log(typeof b); // ReferenceError - let у TDZ var a = 1; let b = 2; ``` Це збиває з пантелику навіть досвідчених розробників. Бачив на code review, як хтось вважав `typeof` завжди безпечним до рядка оголошення. ### Типові помилки **var у циклі** ```javascript for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); // виводить: 3 3 3 } ``` Усі три колбеки замикаються на одному `i`, бо `var` має область видимості функції. Коли вони спрацьовують, цикл вже завершився і `i` дорівнює 3. Рішення: замінити на `let`, яка створює нове прив'язування для кожної ітерації. **Припущення, що var поважає блоки** ```javascript if (true) { var x = 5; } console.log(x); // 5 - витікає з блоку ``` `var` зупиняється тільки на межах функції. Тут потрібна `let` або `const`. **Приховування (shadowing) без усвідомлення** ```javascript const user = 'Alice'; function greet() { const user = 'Bob'; // приховує зовнішній user console.log(user); // "Bob" } greet(); console.log(user); // "Alice" ``` Shadowing - валідний JavaScript, але він ускладнює відстеження коду. Краще перейменуй внутрішню змінну. ### Де це зустрічається - React: `useEffect` замикається на змінних scope компонента. Якщо `users` змінився, новий effect захоплює свіже посилання - Express: обробники маршрутів замикаються на об'єкті `app` зі scope модуля - Node.js: `require` загортає кожен модуль у функцію, даючи йому власну scope для `exports` - Redux thunks: захоплюють `dispatch` зі scope підключеного компонента ### Питання на співбесіді **Q:** Що виведе `console.log(x); var x = 1;`? **A:** `undefined`. Оголошення піднімається на початок scope функції, але ініціалізація залишається на місці. **Q:** Чому `for (var i...)` з `setTimeout` виводить одне число тричі? **A:** `var` створює одне прив'язування, спільне для всіх ітерацій. Усі замикання вказують на той самий `i`. Заміни на `let` - кожна ітерація отримає власне прив'язування. **Q:** Що таке Temporal Dead Zone? **A:** TDZ - це проміжок між моментом, коли змінна `let`/`const` з'являється в scope (початок блоку), і рядком оголошення. Будь-який доступ у цьому проміжку кидає `ReferenceError`. **Q:** Чи стає `let` на верхньому рівні глобальною змінною? **A:** Ні. `var x = 1` на верхньому рівні додає `x` до `window` у браузері. `let x = 1` на верхньому рівні створює змінну з блоковою областю видимості, яка не є властивістю `window`. **Q:** Як V8 оптимізує пошук змінних у гарячих циклах? **A:** V8 використовує inline caching. Після першого пошуку рушій запам'ятовує тип і розташування змінної. Наступні звернення в тій самій scope обходять весь ланцюжок. Це одна з причин, чому вужча область видимості покращує продуктивність. ## Приклади ### Базовий: ланцюжок областей видимості ```javascript const language = 'JavaScript'; function describe() { const topic = 'scope'; function announce() { const detail = 'лексична'; console.log(`${detail} ${topic} у ${language}`); // "лексична scope у JavaScript" // Кожна змінна - з різного рівня вкладеності } announce(); } describe(); ``` `announce` читає `detail` зі своєї scope, `topic` з батьківської функції і `language` з глобальної. Пошук завжди іде зсередини назовні, ніколи навпаки. ### Середній рівень: var проти let у циклі Класичне питання на JavaScript-співбесіді. Дві версії - два різних результати: ```javascript // var: усі замикання поділяють одне прив'язування for (var i = 0; i < 3; i++) { setTimeout(() => console.log('var:', i), 0); } // Виведе: var: 3 var: 3 var: 3 // let: кожна ітерація отримує власне прив'язування for (let j = 0; j < 3; j++) { setTimeout(() => console.log('let:', j), 0); } // Виведе: let: 0 let: 1 let: 2 ``` Різниця повністю в правилах scope. `var` має область видимості функції, тому є один `i` для всього циклу. `let` має блокову область, тому кожне тіло циклу отримує свій `j`, який належить тільки цій ітерації.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.