Jakie jest wyjaśnienie tych dziwnych zachowań JavaScript wspomnianych w wykładzie „Wat” dla CodeMash 2012?

753

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.

NibblyPig
źródło
4
{} + [] Jest w zasadzie jedynym trudnym i zależnym od implementacji, jak wyjaśnię tutaj , ponieważ zależy od tego, czy zostanie on przeanalizowany jako wyrażenie lub wyrażenie. W jakim środowisku testujesz (mam oczekiwane 0 w Firefow i Chrome, ale mam „[object Object]” w NodeJs)?
hugomg
1
Korzystam z przeglądarki Firefox 9.0.1 w systemie Windows 7, a JSFiddle ocenia to na [Obiekt]
NibblyPig
@missingno Dostaję 0 w NodeJS REPL
OrangeDog
41
Array(16).join("wat" - 1) + " Batman!"
Nick Johnson
1
@missingno Wysłałem pytanie tutaj , ale dla {} + {}.
Ionică Bizău

Odpowiedzi:

1479

Oto lista objaśnień dotyczących wyników, które widzisz (i powinny być). Referencje, których używam, pochodzą ze standardu ECMA-262 .

  1. [] + []

    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łania object.toString()( §8.12.8 ). W przypadku tablic jest to to samo, co wywoływanie array.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.

  2. [] + {}

    Podobnie [] + [], oba operandy są najpierw konwertowane na prymitywy. W przypadku „Obiektów obiektów” (§ 15.2) jest to ponownie wynik wywołania object.toString(), którym jest dla obiektów o wartości innej niż zero, nieokreślonej "[object Object]"( §15.2.4.2 ).

  3. {} + []

    {}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 ) powraca ToNumber(ToPrimitive(operand)). Jak już wiemy, ToPrimitive([])jest pusty ciąg znaków, a zgodnie z §9.3.1 , ToNumber("")to 0.

  4. {} + {}

    Podobnie jak w poprzednim przypadku, pierwszy {}jest analizowany jako blok z pustą wartością zwracaną. Ponownie, +{}jest to samo co ToNumber(ToPrimitive({}))i ToPrimitive({})jest "[object Object]"(patrz [] + {}). Aby uzyskać wynik +{}, musimy zastosować ToNumberciąg "[object Object]". Postępując zgodnie z instrukcjami z §9.3.1 , otrzymujemy NaNw rezultacie:

    Jeśli gramatyka nie może zinterpretować ciągu jako rozwinięcia ciągu StringNumericLiteral , wynikiem ToNumber jest NaN .

  5. 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 gdy ToNumber("wat")znowu jest NaNjak w §9.3.1 . Po wykonaniu kroku 7 w § 11.6.2 , § 11.6.3 nakazuje to

    Jeśli jednym z operandów jest NaN , wynikiem jest NaN .

    Tak więc argumentem Array(16).joinjest NaN. Zgodnie z § 15.4.4.5 ( Array.prototype.join) musimy odwołać ToStringsię do argumentu, który brzmi "NaN"( §9.8.1 ):

    Jeśli m to NaN , zwróć ciąg "NaN".

    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" + 1zamiast "wat" - 1jako argumentu operator dodawania konwertuje 1na ciąg znaków zamiast "wat"na liczbę, więc skutecznie wywołuje Array(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.

Ventero
źródło
2
Dlaczego więc [] +1 => „1” i [] -1 => -1?
Rob Elsner,
4
@RobElsner []+1postępuje zgodnie z tą samą logiką, co []+[]tylko z 1.toString()operandem rhs . Dla []-1patrz wyjaśnienie "wat"-1w punkcie 5. Pamiętaj, że ToNumber(ToPrimitive([]))wynosi 0 (punkt 3).
Ventero
4
Wyjaśnienia brakuje / pomija wiele szczegółów. Np. „Przekonwertowanie obiektu (w tym przypadku tablicy) na prymityw zwraca wartość domyślną, która dla obiektów z prawidłową metodą toString () jest wynikiem wywołania object.toString ()” całkowicie brakuje tej wartościOf z [] wywoływana jako pierwsza, ale ponieważ wartość zwracana nie jest prymitywna (jest to tablica), zamiast niej używana jest funkcja toString z []. Poleciłbym
jahav
30

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 piszesz out({} + []), 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:

if (u)
    v;

if (x) {
    y;
    z;
}

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ć:

function out(code) {
    function format(x) {
        return typeof x === "string" ?
            JSON.stringify(x) : x;
    }   
    document.writeln('&gt;&gt;&gt; ' + code);
    document.writeln(format(eval(code)));
}
document.writeln("<pre>");
out('[] + []');
out('[] + {}');
out('{} + []');
out('{} + {}');
out('Array(16).join("wat" + 1)');
out('Array(16).join("wat - 1")');
out('Array(16).join("wat" - 1) + " Batman!"');
document.writeln("</pre>");

[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 ().]

CR Drost
źródło
15
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.
Jess Telford
4
@JessTelford W ES6 można używać letdo deklarowania zmiennych o zasięgu blokowym.
Oriol
19

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, nulllogiczne, 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 obiekt obji wykonywane są następujące kroki:

  1. Zadzwoń obj.valueOf(). Jeśli zwraca prymityw, to koniec. Bezpośrednie wystąpienia Objecti tablice zwracają się same, więc jeszcze nie skończyłeś.
  2. Zadzwoń obj.toString(). Jeśli zwraca prymityw, to koniec. {}i []oba zwracają ciąg, więc gotowe.
  3. W przeciwnym razie rzuć TypeError.

W przypadku dat kroki 1 i 2 są zamieniane. Możesz zaobserwować zachowanie konwersji w następujący sposób:

var obj = {
    valueOf: function () {
        console.log("valueOf");
        return {}; // not a primitive
    },
    toString: function () {
        console.log("toString");
        return {}; // not a primitive
    }
}

Interakcja ( Number()najpierw przekształca się w prymityw, a następnie w liczbę):

> Number(obj)
valueOf
toString
TypeError: Cannot convert object to primitive value

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?

Axel Rauschmayer
źródło
13

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.
  • Wszystkie natywne funkcje lub operatory, które oczekują pierwotnej wartości jako argumentu, najpierw przekonwertują ten argument na pożądany typ pierwotny. Odbywa się to za pomocą valueOflub toString, 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 jak String([]) + 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 jak String([]) + 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, valueOfktóry w przypadku tablic zwraca ten sam obiekt, a następnie próbuje w ostateczności: przekształcić toStringwynik 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ę. Otrzymujemy NaN, a każda operacja arytmetyczna na NaNwynikach z NaN, więc mamy: Array(16).join(NaN).
Mariusz Nowak
źródło
0

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):

  • Użytkownik zamierza połączyć dwa ciągi, wynik: „12”
  • Użytkownik zamierza dodać dwie liczby, wynik: 3

Zatem przy różnych typach wejściowych możliwości wyjściowe rosną.

Algorytm dodawania

  1. Wymuś operandy na prymitywne wartości

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.

  1. Jeśli dowolna wartość argumentu jest łańcuchem, wykonaj konkatenację łańcucha

  2. 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

+-----------------+-------------------+---------------+
| Primitive Value |   String value    | Numeric value |
+-----------------+-------------------+---------------+
| null            | null            | 0             |
| undefined       | undefined       | NaN           |
| true            | true            | 1             |
| false           | false           | 0             |
| 123             | 123             | 123           |
| []              | “”                | 0             |
| {}              | “[object Object]” | NaN           |
+-----------------+-------------------+---------------+

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).

Abdul Fatat Popoola
źródło