Що таке тимчасова зона мертвих (TDZ) у JavaScript
Temporal Dead Zone (TDZ) - це період від початку блочної області видимості до рядка, де ініціалізується let або const. Звернення до змінної в цей час кидає ReferenceError.
Теорія
TL;DR
- TDZ - як полиця з табличкою "зарезервовано": слот існує в пам'яті, але читати його не можна.
varодразу ініціалізується доundefinedпри підйомі (hoisting).let/constзалишаються неініціалізованими до рядка присвоєння.- Звернення до
let/constдо цього рядка даєReferenceError, неundefined. - У сучасному коді використовуй
let/const. TDZ перетворює тихі баги на явні помилки.
Швидкий приклад
console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 5;
console.log(a); // 5
console.log(b); // undefined - немає TDZ для var
var b = 5;Обидві змінні підняті (hoisted). Але var b одразу отримує undefined, тоді як let a залишається неініціалізованою до виконання рядка 2.
Ключова різниця
var підіймається і автоматично ініціалізується до undefined, тому читання до оголошення повертає значення без помилки. let і const підіймають лише байндинг у лексичне середовище (lexical environment), залишаючи його неініціалізованим. Будь-яке читання до рядка присвоєння кидає ReferenceError. TDZ закінчується в момент виконання присвоєння.
Коли що використовувати
- Блочна змінна, що змінюється →
let. - Значення, яке не повинно змінюватись →
const(TDZ плюс заборона перепризначення). - Старий
for-цикл у pre-ES6 бібліотеці →var, і більше жодних причин. - React-хуки, Express-обробники, імпорти модулів →
constза замовчуванням.
Таблиця порівняння
| Змінна | Підіймається | Ініціалізується при підйомі | TDZ | Доступ до ініціалізації |
|---|---|---|---|---|
var | Так | undefined | Ні | Повертає undefined |
let | Так | Ні | Так | ReferenceError |
const | Так | Ні | Так | ReferenceError |
Класи, оголошені через class, мають те саме правило що й let. Звернення до класу до рядка його оголошення теж кидає.
Як це працює в движку
V8 виконує два проходи. Перший підіймає var-оголошення і ставить undefined. Для let/const створює неініціалізований байндинг у записі лексичного середовища (lexical environment record). Другий прохід виконує код рядок за рядком. Читання неініціалізованого байндингу кидає ReferenceError. Ось і все, що таке TDZ на рівні движка.
Типові помилки
Думати, що let взагалі не підіймається:
let x;
console.log(x); // undefined - оголошено, але ще не присвоєно значення
x = 10;
console.log(x); // 10let підіймається. Просто залишається неініціалізованою. Тому рання помилка звучить як "cannot access before initialization", а не "x is not defined".
Деструктурування параметрів без значень за замовчуванням:
function fn({ a }) {
console.log(a);
}
fn(); // ReferenceError - деструктурований байндинг має власний TDZ
fn({}); // undefined - нормальноРішення: function fn({ a = 1 }).
Плутати ReferenceError і TypeError:
const x = 1;
x = 2; // TypeError, не ReferenceErrorReferenceError - це TDZ, до ініціалізації. TypeError - це спроба перепризначити const після завершення TDZ. Різні помилки, різні причини.
Більшість TDZ-багів, які я бачив на code review, з'являються після рефакторингу var на let без перевірки порядку читання в тому ж блоці.
Де зустрічається у реальному коді
- React:
const [state, setState] = useState(null)- TDZ не дасть прочитати стейт до рядка з хуком. - Express:
const id = req.params.idвсередині обробника маршруту - ловить звернення до параметрів до їх парсингу. - Node.js-модулі:
const fs = require('fs')на верхньому рівні - strict mode тримає TDZ по всьому файлу. - Redux:
const action = { type: 'FETCH_USER' }- не даєundefinedпотрапити в reducer.
Питання на співбесіді
Q: Що виведе console.log(x); let x = 5;?
A: ReferenceError: Cannot access 'x' before initialization. Змінна підіймається, але залишається в TDZ до рядка присвоєння.
Q: Чи є TDZ у const?
A: Так, ті самі правила що й у let. TDZ закінчується на рядку присвоєння. Після цього спроба перепризначення кидає TypeError, не ReferenceError.
Q: Навіщо взагалі потрібен TDZ?
A: Щоб зробити ранній доступ до змінної явною помилкою. З var такий доступ повертає undefined без жодного попередження - і це джерело багів, які важко відстежити.
Q: Як Babel імітує TDZ при транспіляції в ES5?
A: Babel використовує перевірку через void 0 як сентинел для неініціалізованого стану. Наближення до специфікації, але в крайніх випадках поведінка може відрізнятись від нативного движка.
Приклади
Базова демонстрація TDZ
// Обидві змінні підняті з початку блоку.
// var: одразу ініціалізується до undefined.
// let: залишається неініціалізованою до рядка оголошення.
console.log(name); // undefined (var - немає TDZ)
var name = 'Alice';
console.log(age); // ReferenceError: Cannot access 'age' before initialization
let age = 30;name повертає undefined, бо var ініціалізується при підйомі. age кидає помилку, бо let залишає байндинг неініціалізованим до рядка 9.
TDZ в обробнику запиту
// Безпечний патерн - доступ після оголошення
function handleLogin(userId) {
if (!userId) throw new Error('Відсутній ID користувача');
const token = generateToken(userId); // TDZ вже закінчився до цього рядка
return token;
}
// Небезпечний патерн - порушення TDZ
function badHandler(userId) {
console.log(token); // ReferenceError - token в TDZ
const token = generateToken(userId);
return token;
}У handleLogin token читається тільки після рядка оголошення. У badHandler читання token до рядка з const одразу кидає помилку. TDZ ловить це в розробці, а не в продакшені.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.