dlaczego `typeof dupa` wywala ReferenceError: dupa is not defined

2

Myślałem, że w świecie JS widziałem już wszystko, ale nie...

Po wklejeniu czegoś takiego w konsoli dostajemy ReferenceError: dupa is not defined:

const dupa = (() => {
  console.log(typeof dupa);
})()

Ok, uznajmy, że dupa jest w trakcie ewaluacji i coś takiego trzeba było zrobić.

Ale od teraz po wpisaniu w konsolę samego typeof dupa ... też dostajemy ReferenceError: dupa is not defined - co rujnuje moją "wiedzę", że typeof jest zawsze "bezpieczne".

Ktoś zna opisy Ecmascript na tyle, żeby wyjaśnić dlaczego akurat tak się dzieje?

5

W sumie fajna ciekawostka.

Wydaje mi się, że kluczem do zrozumienia tego zachowania jest działanie let/const - poza innymi zasadami co do scope'a mają one jeszcze jedną własciwość - przy zadeklarowaniu w globalnej przestrzeni nie wchodzą one w skład obiektu window (lub global w Node.js), ale niejako przesłaniają go, przykład:

var foo = 1
const bar = 2

console.log(foo) // => 1
console.log(window.foo) // => 1

console.log(bar) // => 2
console.log(window.bar) // => undefined

Poza tym do const nie jest nigdy niejawnie przypisywana wartość undefined - zawsze trzeba do niej ręcznie przypisać wartość równocześnie z deklaracją (wiadomo, to istota consta):

let foo // => undefined
const bar // Uncaught SyntaxError: Missing initializer in const declaration

Dodatkowo przy równoczesnej deklaracji i przypisaniu do let i const nie jest przypisywana najpierw wartość undefined:

let foo = 1 // => 1 nie undefined -> 1
const bar = 1 // => 1 nie undefined -> 1

Co się dzieje w konsoli w Twoim przypadku?

> const foo = (() => { throw new Error() })() // => Uncaught Error...
  1. następuje zadeklarowanie zmiennej foo przesłaniającej window.foo
  2. następuje próba przypisania jej wartości, która kończy się niepowodzeniem (rzucenie wyjątku) - żadna wartość (undefined to też wartość) nie zostaje przypisana - zmienna trafia w swoiste "limbo" - identyfikator jest zarezerwowany, wartości brak.

Teraz próbujesz wywołać typeof:

> typeof foo // Uncaught ReferenceError...

Interpreter najpierw ewaluuje zmienną foo - nie ma ona wartości (choć identyfikator istnieje, więc nie weźmie wartości z obiektu window), więc wywala błąd (w przypadku var będzie typ będzie "undefined", bo sięgnie po wartość z window).

Specyfikacja języka z tego co wiem nie definiuje jak ma być zaimplementowany REPL, więc leży to w gestii twórcy przeglądarki - ot zrobili to tak a nie inaczej (mogliby też cofnąć deklarację zmiennej przy błędzie, ale nie wiem czy to lepsze rozwiązanie).

W każdym razie:

co rujnuje moją "wiedzę", że typeof jest zawsze "bezpieczne"

nie jest prawdziwe, bo imo nie da się doprowadzić do takiej sytuacji poza REPL - po rzuceniu wyjątku program się kończy lub wskakuje do bloku catch (o ile jest), więc typeof na takiej felernej zmiennej zadeklarowanej przez const nie da się wykonać (ze względu na scope i brak możliwości redeklaracji), a dla let będzie domyślna wartość undefined:

let foo // zmienna jest deklarowana i niejawnie jest jej przypisywana wartość undefined
try {
  foo = (() => { throw new Error() })() // nowa wartość nie zostaje przypisana, linia rzuca wyjątkiem
  console.log(typeof foo) // ten kod się nie wykona
} catch (e) {
  console.log(typeof foo) // undefined - zgodnie z ostatnim (niejawnym) przypisaniem
}

Mam nadzieję, że nie zagmatwałem jeszcze bardziej :D

Edit:
Jeśli chodzi o tyopeof w samym IFFE to zachodzi tam sytuacja analogiczna - identyfikator jest zarezerwowany, ale wartości nie ma jeszcze - nie może sięgnąć do obiektu window w tamtym momencie bo wyższy priorytet ma const. No ale to też nie wpływa za bardzo na bezpieczeństwo użycia typeof, bo wywala to program od razu (zresztą linter to wyłapie bez problemu).

1

Wspomniane przeze mnie "limbo" ma nawet swoją nazwę - temporal dead zone i okazuje się, że jest to dobrze opisane w specyfikacji ES6: http://exploringjs.com/es6/ch_variables.html#sec_temporal-dead-zone

0

Lepiej działa jak coś zwrócisz.

print("0: ");
const dupa0 = (() => {
  print(typeof dupa0);
})()

print("0: a teraz: ");
print(typeof dupa0);

print("1: ");
const dupa = ((arg) => {
  print(typeof arg);
})(dupa)

print("1: a teraz: ");
print(typeof dupa);

print("2: ");
const dupa2 = ((arg) => {
  print(typeof arg);
  return 0
})(dupa2)

print("2: a teraz: ");
print(typeof dupa2);

Wynik:

0: 
undefined
0: a teraz: 
undefined
1: 
undefined
1: a teraz: 
undefined
2: 
undefined
2: a teraz: 
number

https://ideone.com/sp2JAh

1 użytkowników online, w tym zalogowanych: 0, gości: 1