var, let и const в JavaScript: область видимости и hoisting
Основы3 минРедакция Innovialabs
Коротко
Используйте const по умолчанию и let, когда переменную нужно переприсвоить; избегайте var — он виден во всей функции и поднимается со значением undefined, что порождает тонкие баги.
Короткий ответ
const— блочная область видимости, переприсвоить нельзя. Используйте по умолчанию.let— блочная область видимости, переприсвоить можно. Для счётчиков, накопителей и значений, которые действительно меняются.var— область видимости функции, поднимается со значениемundefined, можно объявить повторно. В новом коде не используйте.
| var | let | const | |
|---|---|---|---|
| Область видимости | функция | блок | блок |
| Переприсвоение | да | да | нет |
| Повторное объявление | да | нет | нет |
| До объявления | undefined | ReferenceError (TDZ) | ReferenceError (TDZ) |
| Обязательная инициализация | нет | нет | да |
Область видимости функции и блока
Блок — всё, что в фигурных скобках: if, for, while или просто { }. let и const живут только внутри блока. var блоки игнорирует и принадлежит всей функции (или глобальной области).
function demo() {
if (true) {
var a = 1;
let b = 2;
}
console.log(a); // 1 — var «протёк» из блока if
console.log(b); // ReferenceError: b is not defined
}
Эта утечка var — корень большинства проблем: переменная, которую вы считали локальной для цикла или условия, видна и изменяема во всей функции.
Hoisting и временная мёртвая зона
Hoisting (поднятие) означает, что движок регистрирует объявления до выполнения кода их области. Поднимаются все три ключевых слова, но по-разному:
varсоздаётся и сразу инициализируется значениемundefined.letиconstсоздаются, но не инициализируются. Пока не выполнится строка с объявлением, переменная находится во временной мёртвой зоне (TDZ), и любое обращение бросаетReferenceError.
console.log(x); // undefined
var x = 5;
console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 5;
TDZ — это полезное поведение: чтение переменной до присвоения значения почти всегда ошибка, и let/const сообщают о ней громко, а не молча отдают undefined.
Объявления функций (function declaration) поднимаются целиком, вместе с телом, поэтому их можно вызвать выше определения. Функциональные выражения, присвоенные const, подчиняются правилам TDZ.
Классический баг с замыканием в цикле
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 3, 3, 3
На всю функцию существует одна переменная i. Колбэки выполняются после окончания цикла, а к этому моменту i равно 3.
С let каждая итерация получает новую привязку, и каждый колбэк захватывает своё значение:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 0, 1, 2
До появления let тело цикла оборачивали в немедленно вызываемую функцию, чтобы создать новую область. Сегодня let решает это одним словом.
const не замораживает объекты
const защищает привязку, а не значение. Присвоить переменной новый объект нельзя, но изменить объект, на который она указывает, можно.
const user = { name: "Aziz" };
user.name = "Dilnoza"; // OK — объект изменён
user = {}; // TypeError: Assignment to constant variable
const list = [1, 2];
list.push(3); // OK
Если нужен неизменяемый объект, используйте Object.freeze() (поверхностная заморозка) или пишите код так, чтобы создавать новые объекты вместо изменения старых.
Простое правило
- Сначала пишите
const. - Меняйте на
let, только когда переменную действительно нужно переприсвоить. - Не используйте
varв новом коде. Это можно закрепить линтером — например, правиламиno-varиprefer-constв ESLint.
Код ещё и читается легче: видите let — значит, значение где-то ниже меняется.
Частые ошибки
- Считать, что
constделает массивы и объекты доступными только для чтения. - Использовать
varв циклах с асинхронными колбэками. - Объявить
letвнутри блока и ожидать, что переменная будет видна после него. - Обращаться к переменной в начале функции, «потому что есть hoisting», — с
let/constэто ошибка. - Случайно повторно объявить
varв длинной функции и затереть значение.