Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Відмінності між var, let та const». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**`var`, `let` та `const`** - три способи оголосити змінну в JavaScript з різними правилами scope та переприсвоєння. ```javascript var x = 1; // функційна область, піднімається як undefined, можна повторно оголосити let y = 2; // блокова область, TDZ до оголошення, не можна повторно оголосити const z = 3; // блокова область, TDZ, не можна переприсвоїти (мутація об'єкта дозволена) ``` **Ключове правило:** `const` за замовчуванням, `let` якщо потрібне переприсвоєння, `var` уникати.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**`var`, `let` та `const`** - три ключові слова для оголошення змінних у JavaScript, кожне з власними правилами scope, підняття (hoisting) та переприсвоєння. ## Теорія ### TL;DR - `var` має функційну область видимості; `let` і `const` - блокову - Всі три піднімаються, але `var` одразу ініціалізується як `undefined`, а `let`/`const` залишаються в тимчасовій мертвій зоні (TDZ) до рядка оголошення - `const` забороняє переприсвоєння; `var` і `let` дозволяють - Правило вибору: `const` за замовчуванням, `let` тільки якщо потрібне переприсвоєння, `var` уникати в новому коді - Аналогія: `var` - дошка в коридорі (видна звідусіль у функції), `let`/`const` - нотатки в конкретній кімнаті (видні тільки в цьому блоці) ### Швидкий приклад ```javascript console.log(varX); // undefined (піднято, ініціалізовано як undefined) var varX = 1; console.log(letX); // ReferenceError: Cannot access 'letX' before initialization let letX = 2; for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); // виводить 3, 3, 3 (спільний var) } for (let j = 0; j < 3; j++) { setTimeout(() => console.log(j), 0); // виводить 0, 1, 2 (окреме прив'язування на ітерацію) } ``` `var` у циклі ділить одну змінну `i` між усіма ітераціями. `let` створює окреме прив'язування на кожній ітерації, тому кожне замикання (closure) захоплює своє власне значення. ### Головна різниця `var` ігнорує блоки `if`, `for` і `while` повністю. Вона належить найближчій функції або глобальному scope. `let` і `const` поважають фігурні дужки: змінна, оголошена всередині блоку, залишається там. Це одне правило пояснює більшість багів, які виникають через витік `var` у зовнішній scope. ### Коли що використовувати - Конфіги, API-ключі, імпортовані модулі → `const` - Лічильники в циклах, мутабельні локальні змінні → `let` - Будь-яке значення, яке буде переприсвоюватись → `let` - Будь-яке значення, яке не змінюється → `const` (включно з об'єктами і масивами) - Підтримка старого legacy-коду → `var` ### Таблиця порівняння | Властивість | `var` | `let` | `const` | |---|---|---|---| | Область видимості | Функція / глобальна | Блок | Блок | | Підняття (hoisting) | Так, ініціалізується як `undefined` | Так, але залишається в TDZ | Так, але залишається в TDZ | | Повторне оголошення | Дозволено | Заборонено | Заборонено | | Переприсвоєння | Дозволено | Дозволено | Заборонено | | Обов'язкова ініціалізація | Ні | Ні | Так | | Коли використовувати | Тільки legacy | Лічильники, мутабельні змінні | За замовчуванням | ### Як компілятор обробляє це У V8 під час компіляції для кожного scope створюється `LexicalEnvironment`. Прив'язування `var` потрапляє у `VariableEnvironment` функції і одразу отримує значення `undefined` - саме тому читання `var` до рядка оголошення повертає `undefined`, а не помилку. `let` і `const` теж потрапляють у `LexicalEnvironment` під час компіляції, але залишаються в неініціалізованому стані. Будь-яке звернення до них до рядка оголошення кидає `ReferenceError`. Цей проміжок між входом у scope і рядком оголошення - це і є TDZ. ### Типові помилки **Помилка 1: `var` у циклах з асинхронними колбеками** ```javascript for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); // виводить 3, 3, 3 } ``` Всі колбеки ділять один `i`, бо `var` функційно-scoped. На момент виконання цикл вже завершився й `i` дорівнює 3. Це класична пастка на співбесіді. Рішення: замінити на `let`. **Помилка 2: `const` не означає незмінність** ```javascript const arr = [1, 2]; arr.push(3); // працює, arr тепер [1, 2, 3] arr = [4, 5]; // TypeError: Assignment to constant variable ``` `const` забороняє переприсвоєння самого прив'язування, але не мутацію значення всередині. Якщо потрібна справжня незмінність - використовуй `Object.freeze()`. **Помилка 3: читання `let` або `const` до оголошення** ```javascript console.log(x); // ReferenceError, не undefined let x = 5; ``` На відміну від `var`, це не повертає `undefined` мовчки. Змінна перебуває в TDZ: рушій знає про неї, але відмовляє в доступі до рядка оголошення. ### Де зустрічається в реальному коді - **React**: `const` для всього - хуки (`const [count, setCount] = useState(0)`), компоненти, імпорти - **Node.js / Express**: `const app = express()`, `const router = express.Router()` - **Redux**: `const FETCH_USER = 'FETCH_USER'` для типів екшенів - **Цикли з лічильником**: `for (let i = 0; i < arr.length; i++)` ### Питання на співбесіді **Q:** Що таке тимчасова мертва зона (TDZ)? **A:** Це проміжок між входом `let`/`const` у scope (під час компіляції) і рядком оголошення. Будь-яке звернення в цьому проміжку кидає `ReferenceError`. У `var` TDZ немає - вона одразу ініціалізується як `undefined`. **Q:** Чи можна змінювати об'єкт, оголошений через `const`? **A:** Так. `const obj = {}; obj.name = 'Alice'` - це нормально. `const` забороняє тільки переприсвоєння прив'язування: `obj = {}` кине TypeError. Якщо потрібна справжня незмінність - `Object.freeze(obj)`. **Q:** Чому `for (var i = 0; ...)` з `setTimeout` завжди виводить фінальне значення? **A:** Бо `var` функційно-scoped і всі ітерації ділять один `i`. Замикання захоплюють посилання на цю змінну, а не копію. Коли колбеки спрацьовують, цикл вже завершився. Заміна на `let` дає окреме прив'язування для кожної ітерації. **Q:** Чи прив'язується `var` до `window` у Node.js? **A:** Ні. У браузері `var` на верхньому рівні потрапляє в `window`. У Node.js кожен файл загортається у функцію модуля, тому `var` залишається локальним у межах цього модуля. **Q:** Поясни на рівні V8, що відбувається з `let` при shadowing у вкладених блоках. **A:** V8 створює новий `LexicalEnvironment` для кожного блоку під час компіляції. Прив'язування зовнішнього `let` потрапляє в зовнішній env. При вході у внутрішній блок створюється другий `LexicalEnvironment`, прив'язаний до зовнішнього. Внутрішній `let` потрапляє туди і перекриває зовнішнє прив'язування. Пошук іде по ланцюжку назовні, тому внутрішній код знаходить своє прив'язування першим. Обидва починаються в TDZ до виконання рядка оголошення. ## Приклади ### Витік scope: var проти let ```javascript function checkScope() { if (true) { var x = 10; // витікає у функційний scope let y = 20; // залишається в цьому блоці } console.log(x); // 10, var витік назовні console.log(y); // ReferenceError, let не витік } checkScope(); ``` `var` не поважає межу блоку `if`. `let` поважає. Одна змінна виходить назовні, інша залишається на місці. Саме ця різниця і є причиною появи `let`. ### React-компонент із правильним вибором оголошень ```javascript function UserList({ users }) { const [filteredUsers, setFilteredUsers] = useState(users); return ( <ul> {filteredUsers.map(user => { const userId = user.id; // новий const на кожній ітерації, переприсвоєння немає return <li key={userId}>{user.name}</li>; })} </ul> ); } ``` Тут все через `const`, бо нічого не переприсвоюється. `setFilteredUsers` - це функція для планування оновлення стану, а не змінна, яку ти перезаписуєш. `userId` - новий блоковий constant на кожній ітерації.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.