Skip to main content

Відмінності між var, let та const

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

Таблиця порівняння

Властивістьvarletconst
Область видимостіФункція / глобальнаБлокБлок
Підняття (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 на кожній ітерації.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?