Rozmawiać „Wata” dla CodeMash 2012 zasadniczo wskazuje na kilka dziwnych dziwactwa z Ruby i JavaScript.
Zrobiłem JSFiddle wyników na http://jsfiddle.net/fe479/9/ .
Zachowania specyficzne dla JavaScript (ponieważ nie znam Rubiego) są wymienione poniżej.
W JSFiddle odkryłem, że niektóre z moich wyników nie odpowiadają tym z filmu i nie jestem pewien, dlaczego. Jestem jednak ciekawy, jak JavaScript radzi sobie z działaniem za kulisami w każdym przypadku.
Empty Array + Empty Array
[] + []
result:
<Empty String>
Jestem dość ciekawy +
operatora, gdy jest on używany z tablicami w JavaScript. To pasuje do wyniku wideo.
Empty Array + Object
[] + {}
result:
[Object]
To pasuje do wyniku wideo. Co tu się dzieje? Dlaczego to jest przedmiot? Co robi +
operator?
Object + Empty Array
{} + []
result:
[Object]
To nie pasuje do filmu. Film sugeruje, że wynik wynosi 0, podczas gdy otrzymuję [Obiekt].
Object + Object
{} + {}
result:
[Object][Object]
To również nie pasuje do wideo, a jak wyprowadzenie zmiennej powoduje powstanie dwóch obiektów? Może mój JSFiddle się myli.
Array(16).join("wat" - 1)
result:
NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
Wykonanie wat + 1 powoduje wat1wat1wat1wat1
...
Podejrzewam, że jest to po prostu zachowanie polegające na tym, że próba odjęcia liczby od łańcucha powoduje powstanie NaN.
źródło
Array(16).join("wat" - 1) + " Batman!"
{} + {}
.Odpowiedzi:
Oto lista objaśnień dotyczących wyników, które widzisz (i powinny być). Referencje, których używam, pochodzą ze standardu ECMA-262 .
[] + []
Podczas korzystania z operatora dodawania zarówno lewy, jak i prawy operand są najpierw konwertowane na prymitywy ( § 11.6.1 ). Zgodnie z §9.1 konwersja obiektu (w tym przypadku tablicy) na prymityw zwraca wartość domyślną, która dla obiektów z prawidłową
toString()
metodą jest wynikiem wywołaniaobject.toString()
( §8.12.8 ). W przypadku tablic jest to to samo, co wywoływaniearray.join()
( § 15.4.4.2 ). Dołączenie pustej tablicy powoduje powstanie pustego ciągu, więc krok # 7 operatora dodawania zwraca konkatenację dwóch pustych ciągów, którym jest pusty ciąg.[] + {}
Podobnie
[] + []
, oba operandy są najpierw konwertowane na prymitywy. W przypadku „Obiektów obiektów” (§ 15.2) jest to ponownie wynik wywołaniaobject.toString()
, którym jest dla obiektów o wartości innej niż zero, nieokreślonej"[object Object]"
( §15.2.4.2 ).{} + []
{}
Tutaj nie jest analizowany jako obiekt, lecz jako pusty blok ( §12.1 , przynajmniej tak długo, jak nie jesteś zmuszając to oświadczenie za wyrażenie, ale o tym później). Zwracana wartość pustych bloków jest pusta, więc wynik tej instrukcji jest taki sam jak+[]
. Jednoargumentowych+
uruchamiający ( §11.4.6 ) powracaToNumber(ToPrimitive(operand))
. Jak już wiemy,ToPrimitive([])
jest pusty ciąg znaków, a zgodnie z §9.3.1 ,ToNumber("")
to 0.{} + {}
Podobnie jak w poprzednim przypadku, pierwszy
{}
jest analizowany jako blok z pustą wartością zwracaną. Ponownie,+{}
jest to samo coToNumber(ToPrimitive({}))
iToPrimitive({})
jest"[object Object]"
(patrz[] + {}
). Aby uzyskać wynik+{}
, musimy zastosowaćToNumber
ciąg"[object Object]"
. Postępując zgodnie z instrukcjami z §9.3.1 , otrzymujemyNaN
w rezultacie:Array(16).join("wat" - 1)
Zgodnie §15.4.1.1 i §15.4.2.2 ,
Array(16)
tworzy nową tablicę o długości 16. Aby uzyskać wartość argumentu do przyłączenia, §11.6.2 kroków # 5 i # 6 pokazują, że mamy do konwersji oba operandy do A numer za pomocąToNumber
.ToNumber(1)
jest po prostu 1 ( §9.3 ), podczas gdyToNumber("wat")
znowu jestNaN
jak w §9.3.1 . Po wykonaniu kroku 7 w § 11.6.2 , § 11.6.3 nakazuje toTak więc argumentem
Array(16).join
jestNaN
. Zgodnie z § 15.4.4.5 (Array.prototype.join
) musimy odwołaćToString
się do argumentu, który brzmi"NaN"
( §9.8.1 ):Po kroku 10 w §15.4.4.5 otrzymujemy 15 powtórzeń konkatenacji
"NaN"
i pustego łańcucha, co odpowiada wynikowi, który widzisz. W przypadku użycia"wat" + 1
zamiast"wat" - 1
jako argumentu operator dodawania konwertuje1
na ciąg znaków zamiast"wat"
na liczbę, więc skutecznie wywołujeArray(16).join("wat1")
.Co do tego, dlaczego widzisz różne wyniki dla
{} + []
przypadku: Gdy używasz go jako argumentu funkcji, wymuszasz, aby instrukcja była wyrażeniem ExpressionStatement , co uniemożliwia parsowanie{}
jako pustego bloku, więc zamiast tego jest analizowany jako pusty obiekt dosłowny.źródło
[]+1
postępuje zgodnie z tą samą logiką, co[]+[]
tylko z1.toString()
operandem rhs . Dla[]-1
patrz wyjaśnienie"wat"-1
w punkcie 5. Pamiętaj, żeToNumber(ToPrimitive([]))
wynosi 0 (punkt 3).To jest bardziej komentarz niż odpowiedź, ale z jakiegoś powodu nie mogę skomentować twojego pytania. Chciałem poprawić kod JSFiddle. Jednak opublikowałem to w Hacker News i ktoś zasugerował, żebym go tutaj opublikował.
Problem w kodzie JSFiddle polega na tym, że
({})
(otwieranie nawiasów w nawiasach) nie jest tym samym co{}
(otwieranie nawiasów jako początek linii kodu). Więc kiedy piszeszout({} + [])
, zmuszasz się{}
do bycia czymś, czego nie ma, kiedy piszesz{} + []
. Jest to część ogólnej „mocy” Javascript.Podstawową ideą było proste JavaScript, który chciał zezwolić na obie te formy:
Aby to zrobić, dokonano dwóch interpretacji nawiasu otwierającego: 1. nie jest wymagany i 2. może pojawić się w dowolnym miejscu .
To był zły ruch. W prawdziwym kodzie nie ma nawiasu otwierającego pojawiającego się w szczerym polu, a także prawdziwy kod jest bardziej delikatny, gdy używa pierwszej formy, a nie drugiej. (Mniej więcej raz na dwa miesiące w mojej ostatniej pracy, dzwoniłem do biurka współpracownika, gdy ich modyfikacje mojego kodu nie działały, a problem polegał na tym, że dodali wiersz do „jeśli” bez dodawania kręconych nawiasy klamrowe. W końcu właśnie przyzwyczaiłem się, że nawiasy klamrowe są zawsze wymagane, nawet jeśli piszesz tylko jedną linię.)
Na szczęście w wielu przypadkach eval () odtworzy pełną JavaScript. Kod JSFiddle powinien brzmieć:
[Również po raz pierwszy pisałem document.writeln od wielu wielu lat i czuję się trochę brudny, pisząc cokolwiek, co dotyczy zarówno document.writeln (), jak i eval ().]
źródło
This was a wrong move. Real code doesn't have an opening brace appearing in the middle of nowhere
- Nie zgadzam się (rodzaj): mam często w ciągu ostatnich używanych bloków jak to do zmiennych zakresów w C . Ten nawyk został na jakiś czas zauważony podczas wykonywania osadzonego C, gdzie zmienne na stosie zajmują miejsce, więc jeśli nie są już potrzebne, chcemy zwolnić miejsce na końcu bloku. Jednak ECMAScript obejmuje tylko bloki funkcji () {}. Tak więc, chociaż nie zgadzam się, że koncepcja jest błędna, zgadzam się, że implementacja w JS jest ( prawdopodobnie ) błędna.let
do deklarowania zmiennych o zasięgu blokowym.Popieram rozwiązanie @ Ventero. Jeśli chcesz, możesz przejść do bardziej szczegółowych informacji o tym, jak
+
konwertuje operandy.Pierwszy krok (§9.1): przekonwertuj oba operandy na prymitywy (wartości pierwotne to
undefined
,null
logiczne, liczby, ciągi; wszystkie inne wartości są obiektami, w tym tablicami i funkcjami). Jeśli operand jest już prymitywny, to koniec. Jeśli nie, jest to obiektobj
i wykonywane są następujące kroki:obj.valueOf()
. Jeśli zwraca prymityw, to koniec. Bezpośrednie wystąpieniaObject
i tablice zwracają się same, więc jeszcze nie skończyłeś.obj.toString()
. Jeśli zwraca prymityw, to koniec.{}
i[]
oba zwracają ciąg, więc gotowe.TypeError
.W przypadku dat kroki 1 i 2 są zamieniane. Możesz zaobserwować zachowanie konwersji w następujący sposób:
Interakcja (
Number()
najpierw przekształca się w prymityw, a następnie w liczbę):Drugi krok (§ 11.6.1): Jeśli jeden z operandów jest ciągiem, drugi operand jest również konwertowany na ciąg, a wynik jest tworzony przez połączenie dwóch łańcuchów. W przeciwnym razie oba operandy są konwertowane na liczby, a wynik jest tworzony przez ich dodanie.
Bardziej szczegółowe wyjaśnienie procesu konwersji: „ Co to jest {} + {} w JavaScript? ”
źródło
Możemy odwoływać się do specyfikacji i jest to świetne i najdokładniejsze, ale większość przypadków można również wyjaśnić w bardziej zrozumiały sposób za pomocą następujących stwierdzeń:
+
a-
operatory działają tylko z pierwotnymi wartościami. Mówiąc dokładniej+
(dodawanie) działa z ciągami znaków lub liczbami, a+
(unary) i-
(odejmowanie i unary) działa tylko z liczbami.valueOf
lubtoString
, które są dostępne na dowolnym obiekcie. To jest powód, dla którego takie funkcje lub operatory nie rzucają błędów, gdy są wywoływane na obiektach.Możemy więc powiedzieć, że:
[] + []
jest taki sam jakString([]) + String([])
który jest taki sam jak'' + ''
. Wspomniałem powyżej, że+
(dodawanie) jest również poprawne dla liczb, ale nie ma prawidłowej reprezentacji liczb w tablicy w JavaScript, więc zamiast tego używane jest dodawanie ciągów.[] + {}
jest taki sam jakString([]) + String({})
który jest taki sam jak'' + '[object Object]'
{} + []
. Ten zasługuje na więcej wyjaśnień (patrz odpowiedź Ventero). W takim przypadku nawiasy klamrowe są traktowane nie jako obiekt, ale jako pusty blok, więc okazuje się, że jest taki sam jak+[]
. Unary+
działa tylko z liczbami, więc implementacja próbuje uzyskać liczbę[]
. Najpierw próbuje,valueOf
który w przypadku tablic zwraca ten sam obiekt, a następnie próbuje w ostateczności: przekształcićtoString
wynik na liczbę. Możemy napisać to jako+Number(String([]))
to samo,+Number('')
co to samo co+0
.Array(16).join("wat" - 1)
odejmowanie-
działa tylko z liczbami, więc jest takie samo jak:,Array(16).join(Number("wat") - 1)
ponieważ"wat"
nie można go przekonwertować na prawidłową liczbę. OtrzymujemyNaN
, a każda operacja arytmetyczna naNaN
wynikach zNaN
, więc mamy:Array(16).join(NaN)
.źródło
Aby podeprzeć to, co zostało wcześniej udostępnione.
Przyczyną tego zachowania jest częściowo słaba czcionka JavaScript. Na przykład wyrażenie 1 + „2” jest dwuznaczne, ponieważ istnieją dwie możliwe interpretacje oparte na typach argumentów (int, string) i (int int):
Zatem przy różnych typach wejściowych możliwości wyjściowe rosną.
Algorytm dodawania
Prymitywy JavaScript to ciąg, liczba, null, niezdefiniowane i logiczne (Symbol wkrótce w ES6). Każda inna wartość jest obiektem (np. Tablice, funkcje i obiekty). Proces przymusu przekształcenia obiektów w wartości pierwotne opisano w następujący sposób:
Jeśli prymitywna wartość zostanie zwrócona po wywołaniu object.valueOf (), zwróć tę wartość, w przeciwnym razie kontynuuj
Jeśli prymitywna wartość jest zwracana po wywołaniu object.toString (), zwróć tę wartość, w przeciwnym razie kontynuuj
Zgłaszanie błędu typu
Uwaga: W przypadku wartości daty kolejność polega na wywołaniu metody toString przed wartościąOf.
Jeśli dowolna wartość argumentu jest łańcuchem, wykonaj konkatenację łańcucha
W przeciwnym razie zamień oba operandy na ich wartości liczbowe, a następnie dodaj te wartości
Znajomość różnych wartości przymusu typów w JavaScript pomaga wyjaśnić mylące wyniki. Zobacz tabelę przymusu poniżej
Warto również wiedzieć, że operator + JavaScript jest skojarzony z lewą stroną, ponieważ określa to, jakie dane wyjściowe będą przypadkami obejmującymi więcej niż jedną operację +.
Wykorzystanie w ten sposób 1 + „2” da „12”, ponieważ każde dodanie zawierające ciąg zawsze będzie domyślnie traktowane jako konkatenacja ciągu.
Możesz przeczytać więcej przykładów w tym poście na blogu (zrzeczenie się, które napisałem).
źródło