Пошук уроків, статей та іншого контенту
Дізнайтеся, чому let і const поводяться інакше за var до моменту декларації. Розберіть Temporal Dead Zone, ReferenceError та зв'язок TDZ із лексичним середовищем.
let, const і varУ JavaScript змінні, оголошені через var, let і const, мають різні правила:
var має функціональну або глобальну область видимості;
let і const мають блочну область видимості;
var до виконання присвоєння має значення undefined;
let і const до моменту ініціалізації перебувають у Temporal Dead Zone — часовій мертвій зоні;
звернення до let або const у TDZ спричиняє ReferenceError.
Розглянемо простий приклад:
console.log(varValue); // undefined
var varValue = 10;
console.log(letValue); // ReferenceError
let letValue = 20;У першому випадку змінна varValue уже створена, але ще має значення undefined.
У другому випадку ім’я letValue уже відоме JavaScript-двигуну, але змінна ще не ініціалізована. До завершення ініціалізації звертатися до неї не можна.
varЗмінна, оголошена через var, доступна в межах функції, у якій її оголошено. Блоки if, for або {} не створюють для var окремої області видимості.
if (true) {
var message = "Привіт";
}
console.log(message); // "Привіт"Якщо var оголошено поза функцією, воно може мати глобальну область видимості.
let і constКожен блок у фігурних дужках створює окрему область видимості для let і const.
if (true) {
let blockMessage = "Привіт";
const blockNumber = 42;
console.log(blockMessage); // "Привіт"
console.log(blockNumber); // 42
}
console.log(blockMessage); // ReferenceError
console.log(blockNumber); // ReferenceErrorЗмінні існують лише всередині блоку, де їх оголошено.
Блоком можуть бути:
тіло функції;
тіло циклу;
тіло умовного оператора;
довільна пара фігурних дужок.
{
let value = 10;
console.log(value); // 10
}
console.log(value); // ReferenceErrorДля розуміння TDZ важливо розрізняти кілька етапів роботи зі змінною:
Створення прив’язки — JavaScript створює ім’я змінної в лексичному середовищі.
Ініціалізація — змінна отримує початкове значення.
Присвоєння — змінній можна надати нове значення, якщо це дозволено.
Для var створення й ініціалізація відбуваються на етапі підготовки виконання коду. Початкове значення — undefined.
Для let і const прив’язка створюється заздалегідь, але ініціалізація відкладається до фактичного виконання декларації.
{
// Прив'язка userName вже створена,
// але змінна ще не ініціалізована.
let userName = "Олена";
// Після виконання декларації змінна доступна.
console.log(userName); // "Олена"
}Саме проміжок між створенням прив’язки та її ініціалізацією називають Temporal Dead Zone.
Temporal Dead Zone, або TDZ, — це частина виконання блоку від входу в область видимості до моменту ініціалізації змінної, оголошеної через let, const або class.
Назва «часова» означає, що йдеться не про фізичне місце в коді, а про проміжок часу під час виконання програми.
{
// Початок TDZ для count
console.log(count); // ReferenceError
let count = 5;
// Кінець TDZ для count
console.log(count); // 5
}Важливо: let count не є повністю невідомою до цього рядка. JavaScript уже зареєстрував ім’я count у поточній області видимості. Проте це ім’я перебуває в неініціалізованому стані.
ReferenceErrorСпроба звернутися до змінної в TDZ спричиняє помилку ReferenceError.
console.log(score);
let score = 100;Результат:
ReferenceError: Cannot access 'score' before initializationЦя помилка відрізняється від ситуації, коли змінну взагалі не оголошено:
console.log(unknownValue);Тут також виникає ReferenceError, але причина інша: імені unknownValue немає в доступному лексичному середовищі.
У повідомленні помилки зазвичай можна побачити різницю:
Cannot access 'score' before initialization — змінна перебуває в TDZ;
unknownValue is not defined — змінну не було оголошено.
varconsole.log(first); // undefined
var first = 1;
console.log(second); // ReferenceError
let second = 2;
console.log(third); // ReferenceError
const third = 3;Умовно це можна уявити так:
// Поведінка var приблизно відповідає:
var first;
console.log(first); // undefined
first = 1;Але для let не можна коректно переписати код як просте переміщення декларації вгору:
// Це НЕ означає, що JavaScript реально виконує код саме так:
let second;
console.log(second);
second = 2;У справжній моделі second до декларації перебуває в неініціалізованому стані, тому читання змінної заборонене.
const і TDZconst має таку саму TDZ, як і let.
console.log(apiUrl); // ReferenceError
const apiUrl = "https://example.com";Крім TDZ, const має додаткове правило: змінній потрібно надати значення під час декларації.
const language = "JavaScript";Такий код некоректний:
const language; // SyntaxErrorПісля ініціалізації змінну const не можна переприсвоїти:
const version = 1;
version = 2; // TypeErrorОднак const не робить об’єкти та масиви повністю незмінними. Заборонене саме переприсвоєння змінної.
const user = {
name: "Олена"
};
user.name = "Марія"; // Дозволено
console.log(user.name); // "Марія"Посилання user усе ще вказує на той самий об’єкт. Змінено властивість об’єкта, а не саме посилання.
typeof не обходить TDZОператор typeof зазвичай не спричиняє помилку для неоголошеного імені:
console.log(typeof notDeclared); // "undefined"Але для змінної в TDZ він також спричиняє ReferenceError:
console.log(typeof value); // ReferenceError
let value = 10;Це важлива відмінність:
typeof notDeclared повертає "undefined";
typeof value у TDZ спричиняє ReferenceError.
JavaScript виконує код у контексті, який можна описати як лексичне середовище. Воно містить:
імена локальних змінних;
значення або стани цих змінних;
посилання на зовнішнє лексичне середовище.
Так JavaScript реалізує області видимості та замикання.
Уявімо виконання цього коду:
let globalValue = "глобальне";
{
let localValue = "локальне";
console.log(globalValue); // "глобальне"
console.log(localValue); // "локальне"
}Для внутрішнього блоку існує власне лексичне середовище. Якщо в ньому немає globalValue, JavaScript шукає це ім’я у зовнішньому середовищі.
Пошук відбувається від внутрішньої області до зовнішньої:
поточний блок;
зовнішній блок;
функція;
глобальне середовище.
Якщо ім’я знайдено, JavaScript перевіряє його стан. Якщо змінна знайдена, але ще не ініціалізована, виникає ReferenceError через TDZ.
Внутрішній блок може оголосити змінну з таким самим ім’ям, як у зовнішньому блоці. Це називається затіненням.
let message = "зовнішнє значення";
{
let message = "внутрішнє значення";
console.log(message); // "внутрішнє значення"
}
console.log(message); // "зовнішнє значення"Але затінення може несподівано спричинити TDZ:
let value = "зовнішнє";
{
console.log(value); // ReferenceError
let value = "внутрішнє";
}Чому не використовується зовнішня змінна?
Під час пошуку імені JavaScript спочатку знаходить локальну прив’язку value. Вона вже приховує зовнішню змінну, але ще не ініціалізована. Тому пошук не продовжується у зовнішньому середовищі.
Фактично код до декларації працює з локальною змінною в TDZ.
TDZ також проявляється, коли локальна змінна затінює глобальну або зовнішню змінну.
const label = "глобальна";
function printLabel() {
console.log(label); // ReferenceError
const label = "локальна";
}
printLabel();Усередині функції JavaScript бачить локальну label. Вона затінює зовнішню label від початку виконання функції до своєї ініціалізації.
Правильний варіант — використовувати зовнішню змінну, якщо локальна не потрібна:
const label = "глобальна";
function printLabel() {
console.log(label); // "глобальна"
}
printLabel();У циклах let часто створює окрему прив’язку для кожної ітерації.
for (let index = 0; index < 3; index += 1) {
setTimeout(() => {
console.log(index);
}, 0);
}Результат:
0
1
2Для кожної ітерації функція отримує відповідну прив’язку index.
З var змінна має одну спільну функціональну прив’язку:
for (var index = 0; index < 3; index += 1) {
setTimeout(() => {
console.log(index);
}, 0);
}Результат:
3
3
3Це не безпосередньо приклад помилки TDZ, але демонструє практичну відмінність лексичної області let від області видимості var.
Параметри функції утворюють власне лексичне середовище. Через це під час використання параметрів за замовчуванням також можуть виникати помилки, пов’язані з недоступним іменем.
function createUser(name = defaultName) {
let defaultName = "Невідомий";
return name;
}
createUser(); // ReferenceErrorПід час обчислення значення параметра name локальна змінна defaultName ще не ініціалізована. Вона не може бути використана як значення за замовчуванням.
Безпечніший варіант:
const defaultName = "Невідомий";
function createUser(name = defaultName) {
return name;
}
console.log(createUser()); // "Невідомий"Наступний приклад можна запустити в Node.js або в консолі браузера:
function demonstrateDeclarations() {
console.log("Порівняння декларацій");
console.log("var до декларації:", varValue);
try {
console.log("let до декларації:", letValue);
} catch (error) {
console.log("let до декларації:", error.name);
}
try {
console.log("const до декларації:", constValue);
} catch (error) {
console.log("const до декларації:", error.name);
}
var varValue = 10;
let letValue = 20;
const constValue = 30;
console.log("var після декларації:", varValue);
console.log("let після декларації:", letValue);
console.log("const після декларації:", constValue);
}
demonstrateDeclarations();
{
const settings = {
theme: "dark"
};
settings.theme = "light";
console.log("Оновлена властивість:", settings.theme);
}Очікуваний результат:
Порівняння декларацій
var до декларації: undefined
let до декларації: ReferenceError
const до декларації: ReferenceError
var після декларації: 10
let після декларації: 20
const після декларації: 30
Оновлена властивість: lightlet і const не піднімаютьсяЧасто кажуть, що let і const «не піднімаються». Це спрощення.
Точніше:
їхні прив’язки створюються під час підготовки лексичного середовища;
до ініціалізації ці прив’язки перебувають у TDZ;
звернення до них до декларації спричиняє ReferenceError.
Тому практичне правило «оголошуйте змінні до використання» залишається правильним, але внутрішній механізм складніший за просте «змінної ще не існує».
console.log(total); // ReferenceError: total is not definedТут total не оголошено.
console.log(total); // ReferenceError: Cannot access 'total' before initialization
let total = 50;Тут total оголошено в поточній області, але воно перебуває в TDZ.
typeof як універсальну перевіркуtypeof безпечно працює з іменами, яких взагалі немає:
console.log(typeof missingVariable); // "undefined"Але він не обходить TDZ:
console.log(typeof declaredLater); // ReferenceError
let declaredLater = true;const незмінним об’єктомconst profile = {
active: false
};
profile.active = true; // Дозволеноconst забороняє переприсвоїти змінну:
profile = {}; // TypeErrorДля незмінності самого об’єкта потрібні додаткові підходи, наприклад створення нового об’єкта замість зміни старого або використання Object.freeze з урахуванням його обмежень.
const status = "ready";
function getStatus() {
console.log(status); // ReferenceError
let status = "loading";
return status;
}Локальна status затінює зовнішню ще до рядка декларації. Щоб уникати таких ситуацій:
використовуйте зрозумілі назви;
не оголошуйте змінні з однаковими іменами в сусідніх областях без потреби;
оголошуйте змінні до першого використання.
У сучасному JavaScript зазвичай дотримуються таких правил:
використовуйте const, якщо змінну не потрібно переприсвоювати;
використовуйте let, якщо значення змінюватиметься;
у новому коді уникайте var, якщо немає конкретної причини його застосовувати;
оголошуйте змінні перед використанням;
не покладайтеся на поведінку var та його значення undefined до декларації;
будьте уважні до затінення змінних у вкладених блоках.
let і const мають блочну область видимості.
var має функціональну область видимості та не враховує звичайні блоки.
Прив’язки let і const створюються до виконання декларації, але залишаються неініціалізованими.
Проміжок до ініціалізації називається Temporal Dead Zone.
Звернення до змінної в TDZ спричиняє ReferenceError.
typeof не обходить TDZ.
Лексичне середовище зберігає прив’язки змінних і посилання на зовнішні середовища.
Локальна змінна може затінити зовнішню навіть до власної декларації.
const забороняє переприсвоєння посилання, але не робить об’єкти та масиви незмінними.
Для нового коду найчастіше обирають const, а let використовують лише тоді, коли значення потрібно змінювати.