ten post jest hołdem dla fantastycznego wykładu Gary ’ ego Bernhardta „Wat”, w którym zwraca uwagę na specyfikę niektórych konstrukcji językowych w Ruby i JavaScript. Jeśli jeszcze nie oglądałeś rozmowy, zdecydowanie polecam poświęcić trochę czasu i zrobić dokładnie to! To tylko jakieś 4 minuty i bardzo zabawne, obiecuję.
w swoim wykładzie Gary pokazuje te cztery fragmenty kodu JavaScript:
widzimy wiele nawiasów, nawiasów i znaków plus. Oto co te fragmenty oceniają na:
+ == ""
+ {} == ""
{} + == 0
{} + {} == NaN
kiedy zobaczyłem te przykłady po raz pierwszy, pomyślałem: „Wow, to wygląda niechlujnie!”Wyniki mogą wydawać się niespójne lub nawet arbitralne, ale proszę o cierpliwość. Wszystkie te przykłady są rzeczywiście bardzo spójne i nie tak źle, jak wyglądają!
#Fragment #1: +
zacznijmy od pierwszego fragmentu:
+ // ""
jak widać, zastosowanie operatora +
do dwóch pustych tablic powoduje powstanie pustego łańcucha. Dzieje się tak, ponieważ reprezentacja łańcuchowa tablicy jest reprezentacją łańcuchową wszystkich jej elementów, połączonych ze przecinkami:
.toString()// "1,2,3".toString()// "1,2".toString()// "1".toString()// ""
pusta tablica nie zawiera żadnych elementów, więc jej reprezentacja łańcuchowa jest pustym łańcuchem. Zatem połączenie dwóch pustych łańcuchów jest po prostu kolejnym pustym łańcuchem.
#Fragment #2: + {}
jak na razie dobrze. Przyjrzyjmy się teraz drugiemu fragmentowi:
+ {}// ""
zauważ, że ponieważ nie mamy do czynienia z dwiema liczbami, operator +
ponownie wykonuje konkatenację łańcuchów zamiast dodawania dwóch wartości liczbowych.
w poprzedniej sekcji widzieliśmy już, że reprezentacja łańcuchów pustej tablicy jest pustym łańcuchem. Łańcuchowa reprezentacja pustego obiektu jest tutaj wartością domyślną ""
. Prepending pusty łańcuch nie zmienia wartości, więc ""
jest wynikiem końcowym.
w JavaScript obiekty mogą zaimplementować specjalną metodę o nazwie toString()
, która zwraca niestandardową reprezentację ciągu znaków obiektu, na którym metoda jest wywoływana. Nasz pusty literal obiektu nie implementuje takiej metody, więc wracamy do domyślnej implementacji prototypu Object
.
#Fragment #3: {} +
twierdzę, że do tej pory wyniki nie były zbyt nieoczekiwane. Po prostu przestrzegali zasad przymusu typu i domyślnych reprezentacji ciągów znaków w JavaScript.
jednak {} +
to tam Programiści zaczynają się mylić:
{} + // 0
dlaczego widzimy 0
(liczba zero), jeśli wpisujemy powyższą linię w REPL JavaScript, jak konsola przeglądarki? Czy wynik nie powinien być ciągiem znaków, tak jak + {}
?
zanim rozwiążemy zagadkę, rozważ trzy różne sposoby użycia operatora +
:
// 1) Addition of two numeric values2 + 2 == 4// 2) String concatenation of two values"2" + "2" == "22"// 3) Conversion of a value to a number+2 == 2+"2" == 2
w pierwszych dwóch przypadkach operator +
jest operatorem binarnym, ponieważ ma dwa operandy (po lewej i po prawej). W trzecim przypadku operator +
jest operatorem jednoargumentowym, ponieważ ma tylko jeden operand (po prawej).
rozważ również dwa możliwe znaczenia {}
w JavaScript. Zwykle zapisujemy {}
jako pusty literał obiektu, ale jeśli znajdujemy się w pozycji polecenia, Gramatyka JavaScript określa {}
jako pusty blok. Poniższy fragment kodu definiuje dwa puste bloki, z których żaden nie jest dosłownym obiektem:
{}// Empty block{ // Empty block}
spójrzmy jeszcze raz na nasz fragment:
{} +
pozwól mi zmienić trochę białe spacje, aby było jaśniejsze, jak silnik JavaScript widzi kod:
{ // Empty block}+;
teraz możemy wyraźnie zobaczyć, co się tu dzieje. Mamy instrukcję block, po której następuje kolejna instrukcja, która zawiera jednoznakowe wyrażenie +
działające na pustej tablicy. Średnik końcowy jest wstawiany automatycznie zgodnie z regułami asi (automatic semicolon insertion).
możesz łatwo sprawdzić w konsoli przeglądarki, że +
ocenia 0
. Pusta tablica ma pusty łańcuch jako swoją reprezentację łańcuchową, która z kolei jest konwertowana na liczbę zero przez operatora +
. Wreszcie, wartość ostatniej instrukcji (w tym przypadku+
) jest zgłaszana przez konsolę przeglądarki.
Alternatywnie, możesz podać oba fragmenty kodu do parsera JavaScript, takiego jak Esprima, i porównać wynikowe abstrakcyjne drzewa składniowe. Oto AST dla + {}
:
{ "type": "Program", "body": }, "right": { "type": "ObjectExpression", "properties": } } } ], "sourceType": "script"}
a oto AST dla {} +
:
{ "type": "Program", "body": }, { "type": "ExpressionStatement", "expression": { "type": "UnaryExpression", "operator": "+", "argument": { "type": "ArrayExpression", "elements": }, "prefix": true } } ], "sourceType": "script"}
zamieszanie wynika z niuansów gramatyki JavaScript, która używa nawiasów klamrowych zarówno dla literałów obiektów, jak i bloków. W pozycji polecenia nawias otwierający rozpoczyna blok, podczas gdy w pozycji wyrażenia nawias otwierający rozpoczyna literał obiektu.
#Fragment #4: {} + {}
na koniec, rzućmy szybko okiem na nasz ostatni fragment {} + {}
:
{} + {}// NaN
cóż, dodanie dwóch literałów obiektowych to dosłownie „nie Liczba” – ale czy dodajemy tutaj dwa literały obiektowe? Nie daj się znowu zwieść aparatom! Oto co się dzieje:
{ // Empty block}+{};
jest to prawie taki sam układ jak w poprzednim przykładzie. Jednak teraz stosujemy Operator uniary plus do pustego literału obiektu. To jest w zasadzie to samo co Robienie Number({})
, co skutkuje NaN
, ponieważ nasz literal obiektu nie może być skonwertowany na liczbę.
jeśli chcesz, aby silnik JavaScript przetwarzał kod jako dwa puste literały obiektu, zawiń pierwszy (lub cały fragment kodu) w nawiasy. Powinieneś teraz zobaczyć oczekiwany wynik:
({}) + {}// ""({} + {})// ""
nawias otwierający powoduje, że parser próbuje rozpoznać wyrażenie, dlatego nie traktuje {}
jako bloku (który byłby instrukcją).
#podsumowanie
powinieneś teraz zobaczyć, dlaczego cztery fragmenty kodu oceniają sposób, w jaki działają. Nie jest to w ogóle arbitralne ani przypadkowe; Zasady przymusu typu są stosowane dokładnie tak, jak określono w specyfikacji i gramatyce języka.
pamiętaj tylko, że jeśli nawias otwierający jest pierwszym znakiem pojawiającym się w instrukcji, będzie on interpretowany jako początek bloku, a nie literał obiektu.