Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Лексичне середовище в JavaScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Лексичне середовище (lexical environment)** - це внутрішня структура JavaScript, яка зберігає прив'язки змінних для конкретного scope і посилання на зовнішній scope, де написано код. ```javascript let x = 10; function log() { console.log(x); } function run() { let x = 20; log(); } run(); // виводить 10, бо log читає зі свого scope визначення ``` **Головне:** scope визначається місцем написання коду, а не місцем виклику.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Лексичне середовище (lexical environment)** - це внутрішня структура JavaScript, яка пов'язує імена змінних з їхніми значеннями для конкретного scope, і зберігає посилання на оточуючий scope, де написаний код. ## Теорія ### TL;DR - Кожна функція, блок `{}` та глобальна область отримують власне лексичне середовище під час виконання - Кожне середовище має дві частини: **Environment Record** (самі змінні) і **зовнішнє посилання** (вказівник на батьківський scope) - Scope визначається місцем написання коду, а не місцем виклику - Ланцюжок зовнішніх посилань - це і є ланцюг областей видимості (scope chain) - Замикання (closure) працюють тому, що функції зберігають посилання на лексичне середовище місця свого створення ### Швидкий приклад ```javascript let city = "Kyiv"; function greet() { let name = "Anna"; console.log(name + " from " + city); // "Anna from Kyiv" } greet(); ``` Коли `greet` виконується, JavaScript створює для неї лексичне середовище. Воно зберігає `name = "Anna"` і має зовнішнє посилання на глобальне середовище, де живе `city = "Kyiv"`. `greet` знаходить `city`, піднімаючись по ланцюжку. ### Дві частини лексичного середовища **Environment Record** зберігає самі прив'язки: змінні, оголошення функцій, параметри. По суті це map з ключами і значеннями для цього scope. **Зовнішнє посилання (outer reference)** вказує на лексичне середовище коду, який *оточує* функцію у вихідному файлі. Не там, де функцію викликають. Там, де її написано. ```javascript function makeCounter() { let count = 0; // зберігається в Environment Record makeCounter return function () { count++; // знаходиться через outer reference до середовища makeCounter return count; }; } const counter = makeCounter(); counter(); // 1 counter(); // 2 ``` Після повернення `makeCounter` її контекст виконання зникає. Але внутрішня функція досі тримає зовнішнє посилання на середовище `makeCounter`. Це середовище залишається живим, бо на нього є активне посилання. Це і є замикання (closure). ### Лексичний проти динамічного scope JavaScript використовує лексичний scope. Змінні шукаються там, де код *написаний*, а не там, де він *виконується*. ```javascript let x = 10; function log() { console.log(x); } function run() { let x = 20; log(); // виводить 10, не 20 } run(); ``` `log` визначена в глобальному scope. Її зовнішнє посилання веде до глобального середовища. `x = 20` всередині `run` для `log` невидима, бо `log` там не шукає. Мови на кшталт Perl або Bash можуть використовувати динамічний scope, де пошук змінних іде по стеку викликів. JavaScript так не робить. ### Як рушій будує ланцюжок Коли функція *створюється* (не викликається), рушій прикріплює поточне лексичне середовище до неї через внутрішній слот `[[Environment]]`. Коли функція *викликається*, для цього виклику створюється нове лексичне середовище, і його зовнішнє посилання встановлюється в те, що зберігається в `[[Environment]]`. Тому scope завжди відповідає місцю визначення. ### Типові помилки **Плутати лексичне середовище з контекстом виконання (execution context).** Контекст виконання - це запис про виклик функції під час роботи програми. Лексичне середовище - один з компонентів цього контексту. Вони пов'язані, але не одне й те саме. **Думати, що scope іде за стеком викликів.** Початківці часто очікують, що `log()`, викликана з `run()`, побачить змінні `run`. Ні. Ланцюжок фіксується в момент створення функції. **Забувати, що `var` ігнорує блочні середовища.** `let` і `const` створюють прив'язки в Environment Record блоку. `var` пропускає блочні середовища і прив'язується до найближчої функції. ```javascript { let blockVar = "блочна змінна"; var funcVar = "функціональна змінна"; } console.log(typeof blockVar); // "undefined" - недоступна поза блоком console.log(funcVar); // "функціональна змінна" ``` **Вважати, що середовище знищується після повернення функції.** Збирач сміття видалить його тільки тоді, коли не залишиться жодного посилання. Живе замикання тримає середовище живим. ### Де зустрічається у реальному коді - **React хуки**: `useState`, `useCallback` і `useEffect` - замикання, які захоплюють значення з лексичного середовища в момент створення. Баги застарілого замикання (stale closure) у `useEffect`, де коллбек читає застаріле значення, - напевно, найпоширеніша проблема, яку я бачу, коли команди переходять з class-компонентів - **Модульний патерн**: IIFE-модулі зберігають приватний стан у закритому лексичному середовищі - **Обробники подій у циклах**: класичний баг з `var` + `setTimeout` трапляється, бо всі обробники ділять одне середовище і читають `i` після завершення циклу - **Мемоізація**: `useMemo` і ручні memoize-функції покладаються на замикання над кешованим результатом ### Follow-up питання **Q:** Яка різниця між лексичним середовищем і ланцюгом областей видимості (scope chain)? **A:** Scope chain - це ланцюжок зовнішніх посилань між лексичними середовищами. Лексичне середовище - окремий вузол цього ланцюжка. Коли JavaScript шукає змінну, він проходить по цьому ланцюжку. **Q:** Коли створюється лексичне середовище? **A:** Нове середовище створюється щоразу, коли викликається функція або починається блок. Кожен виклик функції отримує своє середовище, тому рекурсивні виклики не заважають одне одному. **Q:** Чи може лексичне середовище пережити виклик функції, яка його створила? **A:** Так. Якщо внутрішня функція закривається над ним, середовище залишається живим, поки ця функція доступна. Це і є механізм замикань. **Q:** Що відбувається зі змінними `let` до рядка їхнього оголошення? **A:** Вони існують в Environment Record, але в неініціалізованому стані. Звернення до них до рядка оголошення кидає `ReferenceError`. Цей діапазон називається Temporal Dead Zone (TDZ). **Q:** Чим поведінка `var` відрізняється від `let` щодо лексичних середовищ? **A:** Оголошення `var` піднімаються (hoisting) до Environment Record найближчої функції, а не блоку. Вони також одразу ініціалізуються значенням `undefined`, на відміну від `let` і `const`. ## Приклади ### Замикання, що тримає середовище живим ```javascript function makeAdder(x) { // x живе в Environment Record makeAdder return function (y) { return x + y; // x знаходиться через outer reference }; } const add5 = makeAdder(5); const add10 = makeAdder(10); console.log(add5(3)); // 8 console.log(add10(3)); // 13 ``` `makeAdder` викликано двічі, тому існують два окремі середовища, кожне зі своїм `x`. `add5` і `add10` - замикання над різними середовищами. Тому однаковий аргумент `3` дає різні результати. ### Проблема var у циклі і рішення через let ```javascript // Баг: всі коллбеки ділять одне середовище, i = 3 до моменту їх виклику for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); // виводить 3, 3, 3 } // Рішення: let створює нове блочне середовище на кожній ітерації for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); // виводить 0, 1, 2 } ``` З `var` є одна прив'язка в середовищі функції, і всі три коллбеки вказують на одну й ту саму `i`. З `let` цикл створює нове блочне середовище на кожній ітерації з власним `i`, тому кожне замикання захоплює щось своє.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.