Czy użycie == w JavaScript ma kiedykolwiek sens?

276

W JavaScript, Good Parts , Douglas Crockford napisał:

JavaScript ma dwa zestawy operatorów równości: ===i !==, i ich złych bliźniaków ==i !=. Te dobre działają w oczekiwany sposób. Jeśli dwa operandy są tego samego typu i mają tę samą wartość, wówczas ===produkuje truei !==produkuje false. Źli bliźniacy postępują właściwie, gdy operandy są tego samego typu, ale jeśli są innego rodzaju, próbują wymuszać wartości. Zasady, według których to robią, są skomplikowane i niezapomniane. Oto niektóre z interesujących przypadków:

'' == '0'           // false
0 == ''             // true
0 == '0'            // true

false == 'false'    // false
false == '0'        // true

false == undefined  // false
false == null       // false
null == undefined   // true

' \t\r\n ' == 0     // true

Brak przechodniości jest niepokojący. Moja rada to nigdy nie używać złych bliźniaków. Zamiast tego zawsze używaj ===i !==. Wszystkie przedstawione porównania porównują wyniki falsez ===operatorem.

Biorąc pod uwagę tę jednoznaczną obserwację, czy kiedykolwiek zdarza się, że stosowanie ==może być właściwe?

Robert Harvey
źródło
11
Ma to sens w wielu miejscach. Za każdym razem, gdy jest oczywiste, że porównujesz dwie rzeczy tego samego typu (zdarza się to często z mojego doświadczenia), == vs === po prostu sprowadza się do preferencji. Innym razem faktycznie potrzebujesz abstrakcyjnego porównania (jak przypadek, o którym wspomniałeś w odpowiedzi). To, czy jest to właściwe, zależy od konwencji dla danego projektu.
Hej,
4
Jeśli chodzi o „wiele miejsc”, z mojego doświadczenia wynika, że ​​liczba przypadków, w których nie ma znaczenia, przewyższa liczbę przypadków, w których ma miejsce. Twoje doświadczenie może być inne; może mamy doświadczenie w różnego rodzaju projektach. Kiedy patrzę na projekty, które używają ==domyślnie, ===wyróżnia się i informuje, że dzieje się coś ważnego.
Hej,
4
Nie sądzę, że JavaScript idzie wystarczająco daleko ze swoim przymusem typów. Powinien mieć jeszcze więcej opcji przymusu, podobnie jak język BS .
Mark Booth
5
Jednym z miejsc, których używam ==, jest porównywanie rozwijanych identyfikatorów (które zawsze są znakami) z identyfikatorami modeli (które zwykle są int).
Scottie,
11
Rozwój @DevSolar w sieci Web ma sens, gdy nie chcesz zajmować się tworzeniem aplikacji natywnej dla każdej z 15 platform, a także certyfikacją monopolistycznego sklepu App Store każdej platformy, która ma taką platformę.
Damian Yerrick

Odpowiedzi:

232

Będę argumentować za ==

Cytowany przez ciebie Douglas Crockford znany jest z wielu i często bardzo przydatnych opinii. Chociaż w tym konkretnym przypadku jestem z Crockfordem, warto wspomnieć, że nie jest to jedyna opinia. Są tacy jak twórca języków Brendan Eich, którzy nie widzą dużego problemu ==. Argument wygląda trochę następująco:

JavaScript jest językiem pisanym behawioralnie *. Rzeczy są traktowane na podstawie tego, co mogą zrobić, a nie na ich rzeczywistym typie. Dlatego możesz wywołać metodę tablicy .mapna NodeList lub na zestawie selekcji jQuery. To także dlatego możesz zrobić 3 - "5"i odzyskać coś znaczącego - ponieważ „5” może zachowywać się jak liczba.

Kiedy zachowujesz ==równość, porównujesz zawartość zmiennej, a nie jej typ . Oto kilka przypadków, w których jest to przydatne:

  • Czytanie numeru od użytkownika - czy czytać .valueelement wejściowy w DOM? Nie ma problemu! Nie musisz rzucać go ani martwić się o jego typ - możesz ==od razu zacząć od liczb i odzyskać coś znaczącego.
  • Chcesz sprawdzić „istnienie” deklarowanej zmiennej? - możesz to == nullzrobić, ponieważ behawioralnie nulloznacza, że ​​nic tam nie ma, a niezdefiniowane też tam nie ma.
  • Chcesz sprawdzić, czy użytkownik uzyskał znaczący wkład od użytkownika? - sprawdź, czy podany ==argument jest fałszywy , potraktuje przypadki, w których użytkownik nic nie wprowadził lub po prostu spacje dla Ciebie, co prawdopodobnie jest tym, czego potrzebujesz.

Spójrzmy na przykłady Crockforda i wyjaśnijmy je behawioralnie:

'' == '0'           // got input from user vs. didn't get input - so false
0 == ''             // number representing empty and string representing empty - so true
0 == '0'            // these both behave as the number 0 when added to numbers - so true    
false == 'false'    // false vs got input from user which is truthy - so false
false == '0'        // both can substitute for 0 as numbers - so again true

false == undefined  // having nothing is not the same as having a false value - so false
false == null       // having empty is not the same as having a false value - so false
null == undefined   // both don't represent a value - so true

' \t\r\n ' == 0     // didn't get meaningful input from user vs falsey number - true 

Zasadniczo ==jest zaprojektowany do pracy w oparciu o zachowanie prymitywów w JavaScript, a nie o to, czym . Chociaż osobiście nie zgadzam się z tym punktem widzenia, zdecydowanie warto to zrobić - zwłaszcza jeśli weźmiesz ten paradygmat traktowania typów opartych na zachowaniu w całym języku.

* niektórzy wolą nazwę typowania strukturalnego, która jest bardziej powszechna, ale istnieje różnica - tak naprawdę nie interesuje jej tutaj omawianie różnic.

Benjamin Gruenbaum
źródło
8
To świetna odpowiedź i używam wszystkich trzech twoich przypadków użycia „for ==”. # 1 i # 3 są szczególnie przydatne.
Chris Cirefice,
223
Problem ==nie polega na tym, że żadne z jego porównań nie jest użyteczne , ale na tym, że reguł nie da się zapamiętać, więc masz prawie gwarancję popełnienia błędów. Na przykład: „Chcesz sprawdzić, czy użytkownik dostarczył znaczące dane wejściowe od użytkownika?”, Ale „0” jest znaczącym wejściem i '0'==falsejest prawdziwe. Gdybyś skorzystał ===, musiałbyś wyraźnie o tym pomyśleć i nie popełniłbyś błędu.
Timmmm,
44
„reguł nie można zapamiętać” <== to jedna rzecz, która odstrasza mnie od robienia czegokolwiek „znaczącego” w JavaScript. (i matematyka zmiennoprzecinkowa, która prowadzi do problemów w podstawowych ćwiczeniach
kalkulacyjnych
9
Ten 3 - "5"przykład sam w sobie podnosi dobrą rację: nawet jeśli używasz wyłącznie ===do porównania, tak właśnie działają zmienne w JavaScript. Nie ma sposobu, aby całkowicie go uciec.
Jarett Millard
23
@Timmmm note Gram tutaj jako adwokat diabła. Nie używam ==w swoim własnym kodzie, uważam go za anty-wzór i całkowicie zgadzam się, że algorytm abstrakcyjnej równości jest trudny do zapamiętania. To, co tu robię, polega na argumentowaniu, że ludzie to robią.
Benjamin Gruenbaum
94

Okazuje się, że jQuery używa konstrukcji

if (someObj == null) {
  // do something
}

obszernie, jako skrót dla równoważnego kodu:

if ((someObj === undefined) || (someObj === null))  {
  // do something
}

Jest to konsekwencja specyfikacji języka skryptowego ECMAS § 11.9.3, Algorytm porównania równości abstrakcyjnej , który stwierdza między innymi, że

1.  If Type(x) is the same as Type(y), then  
    a.  If Type(x) is Undefined, return true.  
    b.  If Type(x) is Null, return true.

i

2.  If x is null and y is undefined, return true.
3.  If x is undefined and y is null, return true.

Ta szczególna technika jest na tyle powszechna, że JSHint ma specjalnie do niej zaprojektowaną flagę .

Robert Harvey
źródło
10
Nie ma uczciwej odpowiedzi na twoje pytanie Chciałem na to odpowiedzieć :) == null or undefinedto jedyne miejsce, w którym nie korzystam ===lub!==
proszę,
26
Szczerze mówiąc, jQuery nie jest modelową bazą kodu. Po kilkukrotnym przeczytaniu źródła jQuery jest to jedna z moich najmniej ulubionych baz kodowych z wieloma zagnieżdżonymi trójskładnikami, niejasnymi bitami, zagnieżdżaniem i rzeczami, których w innym przypadku unikałbym w prawdziwym kodzie. Nie wierz mi jednak na słowo - przeczytaj github.com/jquery/jquery/tree/master/src, a następnie porównaj z Zepto, który jest klonem jQuery: github.com/madrobby/zepto/tree/master/src
Benjamin Gruenbaum,
4
Zauważ też, że Zepto wydaje się domyślnie ==używać tylko wtedy, ===gdy jest to potrzebne: github.com/madrobby/zepto/blob/master/src/event.js
Hej
2
@ Hej, żeby być uczciwym Zepto nie jest też modelową bazą kodu - jest niesławny z powodu używania __proto__i z kolei zmuszania go prawie do samodzielnego wprowadzania specyfikacji języka, aby uniknąć uszkodzenia stron mobilnych.
Benjamin Gruenbaum
2
@BenjaminGruenbaum, który nie był osądem w sprawie jakości ich bazy kodu, wskazując tylko, że różne projekty są zgodne z różnymi konwencjami.
Hej,
15

Sprawdzanie wartości dla nulllub undefinedjest jedną rzeczą, jak zostało to obficie wyjaśnione.

Jest jeszcze jedna rzecz, w której ==błyszczy:

Możesz zdefiniować porównanie w ten >=sposób (ludzie zwykle zaczynają od, >ale uważam to za bardziej eleganckie):

  • a > b <=> a >= b && !(b >= a)
  • a == b <=> a >= b && b >= a
  • a < bi a <= bsą pozostawione czytelnikowi jako ćwiczenie.

Jak wiemy, w JavaScript "3" >= 3i "3" <= 3, z którego otrzymujesz 3 == "3". Możesz stwierdzić, że strasznym pomysłem jest umożliwienie implementacji porównywania ciągów z liczbami przez ich parsowanie. Ale biorąc pod uwagę, że jest to sposób to działa, ==jest absolutnie poprawny sposób realizacji tego operatora relacji.

Naprawdę dobrą rzeczą ==jest to, że jest spójna ze wszystkimi innymi związkami. Innymi słowy, jeśli napiszesz to:

function compare(a, b) {
  if (a > b) return 1;
  if (a < b) return -1;
  return 0;
}

Używasz domyślnie ==już.

Teraz do dość pokrewnego pytania: czy sposób implementacji porównania liczb i ciągów był zły? Z perspektywy czasu wydaje się to dość głupie. Ale w kontekście innych części JavaScript i DOM jest to stosunkowo pragmatyczne, biorąc pod uwagę, że:

  • atrybuty są zawsze łańcuchami
  • klucze są zawsze ciągami znaków (przypadek użycia polega na tym, że używasz an, Objectaby uzyskać rzadką mapę od wartości int do wartości)
  • wartości wejściowe i kontrolne formularza są zawsze ciągami (nawet jeśli źródło pasuje input[type=number])

Z wielu powodów sensowne było, aby w razie potrzeby łańcuchy zachowywały się jak liczby. Zakładając, że porównywanie i konkatenacja łańcuchów zawierały różne operatory (np. ::Do konkatowania i metodę porównywania (gdzie można użyć wszelkiego rodzaju parametrów dotyczących rozróżniania wielkości liter i co nie)), w rzeczywistości byłby to mniejszy bałagan. Ale przeciążenie tego operatora jest prawdopodobnie w rzeczywistości tym, skąd pochodzi „Java” w „JavaScript”;)

back2dos
źródło
4
Uczciwość >=nie jest tak naprawdę przechodnia. W JS jest całkiem możliwe, że ani a > bani a < bani b == a(na przykład NaN:).
Benjamin Gruenbaum,
8
@BenjaminGruenbaum: To tak, jakby powiedzieć, że +nie jest tak naprawdę przemienny, ponieważ NaN + 5 == NaN + 5nie ma znaczenia. Chodzi o to, że >=działa z wartościami liczbowymi, dla których ==działa konsekwentnie. Nie powinno dziwić, że „nie liczba” z natury nie jest liczbą;)
back2dos
4
Więc złe zachowanie ==jest zgodne ze złym zachowaniem >=? Świetnie, teraz żałuję, że nie było >==...
Eldritch Conundrum
2
@EldritchConundrum: Jak próbowałem wyjaśnić, zachowanie >=jest raczej spójne z resztą interfejsu API języka / standardu. W sumie JavaScript jest czymś więcej niż tylko sumą dziwacznych części. Jeśli chcesz >==, czy chciałbyś również surowego +? W praktyce wiele z tych decyzji znacznie ułatwia wiele rzeczy. Więc nie spieszyłbym się z osądzaniem ich jako biednych.
back2dos
2
@EldritchConundrum: Ponownie: operatory relacji służą do porównywania wartości liczbowych, przy czym jeden operand może być w rzeczywistości łańcuchem. Dla typów operandów, których >=znaczenie ==jest równie ważne, to wszystko - to wszystko. Nikt nie mówi, należy porównać [[]]z false. W językach takich jak C wynikiem tego poziomu bzdur jest niezdefiniowane zachowanie. Po prostu traktuj to w ten sam sposób: nie rób tego. I wszystko będzie dobrze. Nie musisz też pamiętać żadnych magicznych zasad. A potem jest to raczej proste.
back2dos
8

Jako profesjonalny matematyk widzę w operatorze podobieństwa Javscript == ( zwanym także „porównaniem abstrakcyjnym”, „luźną równość” ) próbę zbudowania relacji równoważności między jednostkami, która obejmuje bycie zwrotnym , symetrycznym i przechodnim . Niestety dwie z tych trzech podstawowych właściwości zawodzą:

==nie jest zwrotny :

A == A może być fałszywe, np

NaN == NaN // false

==nie jest przechodnie :

A == Bi B == Crazem nie oznaczają A == C, np

'1' == 1 // true
1 == '01' // true
'1' == '01' // false

Przetrwa tylko własność symetryczna :

A == Bsugeruje B == A, które naruszenie jest w każdym razie nie do pomyślenia i doprowadziłoby do poważnego buntu;)

Dlaczego stosunki równoważności mają znaczenie?

Ponieważ jest to najważniejszy i najczęstszy rodzaj relacji, poparty licznymi przykładami i aplikacjami. Najważniejszą aplikacją jest rozkład jednostek na klasy równoważności , co samo w sobie jest bardzo wygodnym i intuicyjnym sposobem rozumienia relacji. Brak równoważności prowadzi do braku klas równoważności, co z kolei prowadzi do znanego braku intuicyjności i niepotrzebnej złożoności.

Dlaczego tak okropny jest pomysł na pisanie ==relacji nierównomierności?

Ponieważ niszczy naszą znajomość i intuicję, ponieważ dosłownie każda interesująca relacja podobieństwa, równości, kongruencji, izomorfizmu, tożsamości itp. Jest równoważna.

Konwersja typu

Zamiast polegać na intuicyjnej równoważności, JavaScript wprowadza konwersję typu:

Operator równości konwertuje operandy, jeśli nie są tego samego typu, a następnie stosuje ścisłe porównanie.

Ale jak definiuje się konwersję typu? Poprzez zestaw skomplikowanych zasad z licznymi wyjątkami?

Próba zbudowania relacji równoważności

Booleany. Oczywiście truei falsenie są takie same i powinny być w różnych klasach.

Liczby. Na szczęście równość liczb jest już dobrze zdefiniowana, w której dwie różne liczby nigdy nie należą do tej samej klasy równoważności. To znaczy w matematyce. W JavaScript pojęcie liczby jest nieco zdeformowane przez obecność bardziej egzotycznych -0, Infinityi -Infinity. Nasza intuicja matematyczna dyktuje to 0i -0powinna należeć do tej samej klasy (w rzeczywistości -0 === 0jest true), podczas gdy każda z nieskończoności jest odrębną klasą.

Liczby i liczby booleańskie. Biorąc pod uwagę klasy liczbowe, gdzie umieszczamy booleany? falsestaje się podobny do 0, podczas gdy truestaje się podobny do, 1ale nie ma innej liczby:

true == 1 // true
true == 2 // false

Czy jest jakaś logika tutaj umieścić truerazem z 1? Wprawdzie 1wyróżnia się, ale tak też jest -1. Ja osobiście nie widzę żadnego powodu, aby przekształcić truesię 1.

I staje się jeszcze gorzej:

true + 2 // 3
true - 1 // 0

Tak więc truerzeczywiście jest przeliczany na 1wszystkie liczby! Czy to logiczne? Czy to jest intuicyjne? Odpowiedź pozostawia się jako ćwiczenie;)

Ale co z tym:

1 && true // true
2 && true // true

Jedyna logiczna xz x && trueistoty truejest x = true. Co dowodzi, że zarówno 1i 2(i każda inna liczba niż 0) przekształcają się true! Pokazuje to, że nasza konwersja zawodzi inna ważna właściwość - biject . Oznacza to, że dwa różne podmioty mogą konwertować na ten sam. Co samo w sobie nie musi stanowić dużego problemu. Duży problem powstaje, gdy używamy tej konwersji do opisania relacji „identyczności” lub „luźnej równości” tego, co chcemy to nazwać. Ale jedno jest jasne - nie będzie to relacja równoważności i nie będzie intuicyjnie opisana za pomocą klas równoważności.

Ale czy możemy zrobić lepiej?

Przynajmniej matematycznie - zdecydowanie tak! Prosty relacja równoważności między logicznych i numery mogą być zbudowane z tylko falsei 0będąc w tej samej klasie. Tak false == 0byłoby tylko nietrywialne luźne równość.

Co z ciągami znaków?

Możemy przycinać ciągi znaków z białych znaków na początku i na końcu, aby konwertować na liczby, a także możemy ignorować zera z przodu:

'   000 ' == 0 // true
'   0010 ' == 10 // true

Otrzymujemy więc prostą regułę dla ciągu - przycinaj białe spacje i zera z przodu. Otrzymujemy liczbę lub pusty ciąg, w którym to przypadku konwertujemy na tę liczbę lub zero. Lub nie otrzymujemy numeru, w którym to przypadku nie dokonujemy konwersji, więc nie otrzymujemy żadnej nowej relacji.

W ten sposób moglibyśmy uzyskać idealną relację równoważności całkowitego zestawu wartości logicznych, liczb i ciągów! Tyle że ... projektanci JavaScript mają oczywiście inną opinię:

' ' == '' // false

Zatem dwa ciągi, na które oba konwertują, 0nagle stają się niepodobne! Dlaczego lub dlaczego Zgodnie z regułą struny są luźno równe dokładnie wtedy, gdy są ściśle równe! Ta reguła nie tylko łamie przechodniość, jak widzimy, ale także jest zbędna! Po co tworzyć innego operatora, ==aby był ściśle identyczny z drugim ===?

Wniosek

Luźny operator równości ==mógłby być bardzo przydatny, gdyby przestrzegał podstawowych praw matematycznych. Ale jak to niestety nie działa, jego użyteczność cierpi.

Dmitri Zaitsev
źródło
Co NaN? Ponadto, o ile nie zostanie wymuszony określony format liczb do porównywania z ciągami znaków, musi wystąpić nieintuicyjne porównanie ciągu znaków lub brak przechodniości.
Solomon Ucko
@SolomonUcko NaNdziała jako zły obywatel :-). Przejrzystość może i powinna być utrzymana dla każdego porównania równoważności, intuicyjnego lub nie.
Dmitri Zaitsev
7

Tak, natrafiłem na przypadek użycia dla niego, a mianowicie podczas porównywania klucza z wartością liczbową:

for (var key in obj) {
    var some_number = foo(key, obj[key]);  // or whatever -- this is just an example
    if (key == some_number) {
        blah();
    }
}

Myślę, że bardziej naturalne jest porównywanie key == some_numberniż jako Number(key) === some_numberlub jako key === String(some_number).

Mehrdad
źródło
3

Dzisiaj spotkałem całkiem przydatną aplikację. Jeśli chcesz porównać liczby dopełniane, jak 01normalne liczby całkowite, ==działa dobrze. Na przykład:

'01' == 1 // true
'02' == 1 // false

Oszczędza to usunięcia 0 i zamiany na liczbę całkowitą.

Jon Snow
źródło
4
Jestem prawie pewien, że „właściwym” sposobem na to jest '04'-0 === 4, a możeparseInt('04', 10) === 4
ratbum
Nie wiedziałem, że możesz to zrobić.
Jon Snow
7
Dostaję tego mnóstwo.
Jon Snow
1
@ratbum lub+'01' === 1
Eric Lagergren
1
'011' == 011 // falsew trybie ścisłym, a SyntaxError w trybie ścisłym. :)
Brian S
3

Wiem, że to późna odpowiedź, ale wydaje się, że niektóre możliwe zamieszania nulli undefined, co IMHO jest to, co czyni ==zło, tym bardziej, że brak przechodniości, który jest na tyle złe. Rozważać:

p1.supervisor = 'Alice';
p2.supervisor = 'None';
p3.supervisor = null;
p4.supervisor = undefined;

Co to znaczy?

  • p1 ma przełożonego o imieniu „Alice”.
  • p2 ma przełożonego o imieniu „Brak”.
  • p3wyraźnie, jednoznacznie, nie ma przełożonego .
  • p4może lub może mieć przełożonego. Nie wiemy, nie obchodzi nas to, nie powinniśmy wiedzieć (kwestia prywatności?), Ponieważ to nie jest nasza sprawa.

Kiedy używasz == jesteś utożsamiając nulli undefinedktóra jest całkowicie niewłaściwe. Te dwa terminy oznaczają zupełnie inne rzeczy! Mówienie, że nie mam przełożonego, po prostu dlatego, że odmówiłem powiedzenia, kto jest moim przełożonym, jest błędne!

Rozumiem, że są programiści, którzy nie dbają o tę różnicę między nulliundefined czy zdecydują się używać tych terminów inaczej. A jeśli twój świat nie używa nulli undefinedpoprawnie, lub chcesz dać własną interpretację tych warunków, niech tak będzie. Nie sądzę jednak, żeby to był dobry pomysł.

A tak przy okazji, nie mam problemu z nulliundefined oba są falsy! Można całkowicie powiedzieć

if (p.supervisor) { ... }

a potem nulli undefinedspowodowałoby kod, który przetwarza przełożonego zostać pominięty. To prawda, ponieważ nie wiemy lub nie mamy przełożonego. Wszystko dobrze. Ale dwie sytuacje nie są równe . Właśnie dlatego ==się myli. Znowu rzeczy mogą być fałszywe i używane w znaczeniu pisania kaczego, co jest idealne dla dynamicznych języków. To jest poprawny JavaScript, Python, Rubyish itp. Ale znowu, te rzeczy NIE są równe.

I nie zaczynaj mi brak przechodniości: "0x16" == 10,10 == "10" ale nie "10" == "0x16". Tak, JavaScript jest słabym typem. Tak, to jest przymus. Ale przymus nigdy nie powinien nigdy odnosić się do równości.

Nawiasem mówiąc, Crockford ma mocne opinie. Ale wiesz co? Ma rację tutaj!

FWIW Rozumiem, że istnieją i osobiście wpadłem na sytuacje, w których == jest to wygodne! Jak pobieranie ciągów znaków dla liczb i, powiedzmy, porównywanie do 0. Jednak to jest hack. Masz wygodę jako kompromis dla niedokładnego modelu świata.

TL; DR: fałsz to świetna koncepcja. Nie powinna rozciągać się na równość.

Ray Toal
źródło
Dziękujemy za pokazanie różnych sytuacji :) Brakuje jednak p5... typeof(p5.supervisor) === typeof(undefined)
jedynej