TDZ
Piekło zamarzło! Przyszedł dzień, w którym Comandeer posypuje głowę popiołem i przyznaje się do błędu.
Mea culpa
W swojej książce, JavaScript. Programowanie zaawansowane, w rozdziale 2., na stronie 32 stwierdzam:
Jak zatem widać, nowa metoda deklaracji zmiennych działa w sposób, o który od wieków upominali się programiści JS: zmienne ograniczone zostały do bloków, w których je zadeklarowano. Co więcej, żeby już całkowicie “unormalnić” ten obszar JS-a, zrezygnowano z hoistingu (wynoszenia).
Ten fragment (a zwłaszcza drugie zdanie) jest niepoprawny i wynika to ze zbytniego uproszczenia, jakiego się dopuściłem.
Temporal Dead Zone, czyli hoisting bez hoistingu
Doskonale wiadomo, że zmienne deklarowane przy pomocy var
są hoistowane (wynoszone). Rozpatrzmy taki, klasyczny, przykład:
( function() {
console.log( typeof a ); // undefined
var a = 1;
}() );
Jak widać, można pobrać typ zmiennej przed jej deklaracją. Umożliwia to właśnie mechanizm hoistingu, który wszystkie deklaracje zmiennych “wynosi” na sam początek danego scope, zostawiając na miejscu jedynie przypisanie wartości do zmiennej. Powyższy przykład jest zatem widziany przez parser JS mniej więcej tak:
( function() {
var a;
console.log( typeof a ); // undefined
a = 1;
}() );
Gdy zamienimy var
na let
zauważymy, że zachowanie skryptu się znacząco zmienia:
( function() {
console.log( typeof a ); // Uncaught ReferenceError: a is not defined
let a = 1;
}() );
Wniosek, który się nasuwa, jest oczywisty: zmienne deklarowane przez let
nie są hoistowane. I gdybyśmy poprzestali na tego typu przykładzie, faktycznie można by tak przyjąć. Spójrzmy jednak na ciut inną sytuację:
( function() {
var a = 1;
( function() {
console.log( typeof a ); // undefined
var a = 'hublabubla';
}() );
}() );
W tym przykładzie po raz kolejny pojawia się undefined
zamiast number
. Dlaczego? Bo zmienne są hoistowane na górę najbliższego scope. W tym wypadku wewnętrzna funkcja stanowi osobny scope, stąd przesłania liczbową zmienną z zewnętrznego scope.
Jeśli założymy, że zmienne deklarowane przy pomocy let
faktycznie nie są hoistowane, to w przykładzie z let
powinniśmy uzyskać number
(bo deklaracja let
jest dopiero po console.log
, zatem do tego czasu powinna być dostępna zmienna z wyższego scope). Sprawdźmy:
( function() {
let a = 1;
( function() {
console.log( typeof a ); // Uncaught ReferenceError: a is not defined
let a = 'hublabubla';
}() );
}() );
Co tu się stało? Ano, nadzialiśmy się na tzw. Temporal Dead Zone (Czasowo Martwa Strefa). Choć nazwa brzmi strasznie, sam mechanizm aż tak straszny nie jest. Składa się on tak naprawdę z dwóch elementów:
- Zmienne deklarowane przy pomocy
let
(ale takżeconst
) są hoistowane na górę najbliższego scope. - Od początku scope aż do miejsca faktycznej deklaracji zmiennej istnieje TDZ, uniemożliwiając dostęp do zmiennej.
Mechanizm ten wprowadzono dla const
, aby uniemożliwić nadpisanie stałej wewnątrz danego scope i przeniesiono następnie to zachowanie także dla let
– dla zachowania spójności.
Po więcej informacji o TDZ odsyłam – jak zawsze – do odpowiedniego fragmentu twórczości Rauschmayera.
Mea maxima culpa
Jak widać, TDZ w większości przypadków zachowuje się tak, jakby zmienne let
i const
nie były hoistowane. Stąd od biedy fragment w mojej książce mógłby brzmieć:
Co więcej, żeby już całkowicie “unormalnić” ten obszar JS-a, zmienne te w przeważającej większości przypadków zachowują się, jakby nie podlegały hoistingowi [tutaj przypis wspominający o TDZ i odsyłający do Exploring JS].
Posypuję głowę popiołem i przyznaję: pominięcie opisu TDZ było sporym przeoczeniem z mojej strony. Jedyne, co mogę zrobić na swoje usprawiedliwienie, to zacytować Konrada:
Język kłamie głosowi, a głos myślom kłamie;