Відмінності між 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- нотатки в конкретній кімнаті (видні тільки в цьому блоці)
Швидкий приклад
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 у циклах з асинхронними колбеками
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // виводить 3, 3, 3
}Всі колбеки ділять один i, бо var функційно-scoped. На момент виконання цикл вже завершився й i дорівнює 3. Це класична пастка на співбесіді. Рішення: замінити на let.
Помилка 2: const не означає незмінність
const arr = [1, 2];
arr.push(3); // працює, arr тепер [1, 2, 3]
arr = [4, 5]; // TypeError: Assignment to constant variableconst забороняє переприсвоєння самого прив'язування, але не мутацію значення всередині. Якщо потрібна справжня незмінність - використовуй Object.freeze().
Помилка 3: читання let або const до оголошення
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
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-компонент із правильним вибором оголошень
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 на кожній ітерації.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.