Řešení nátlaku, nikoli symptomů

TL;DR

Vaše stížnosti ze dne x == y chování, které je podivné, chybné nebo vyloženě nefunkční, bylo obviňováno == jako viníka. Ne, to opravdu není. == je vlastně docela užitečné.

Problémy, které máte, se netýkají == samotný operátor, ale se základními hodnotami a tím, jak se vnucují různým typům, zejména v podivných rohových případech.

Namísto řešení problémů tím, že se vyhnete == (a vždy pomocí === ), měli bychom zaměřit své úsilí buď na to, abychom se vyhnuli – nebo napravili! – nátlaku na rohový případ hodnoty. Ve skutečnosti odtud všechny WTF skutečně pocházejí.

Rychlý skok:

  • První sekce – Yay Coercion :)
  • Druhá sekce – Boo donucení :(
  • Třetí oddíl – nátlak opraven!

Tento příspěvek oznamuje vydání nejnovější knihy v mém You Don't Know JS knižní série, YDKJS:Types &Grammar , kterou si můžete zdarma přečíst online!

Typy a gramatika obsahuje Předmluvu od našeho úžasného Davida Walshe a je také k dispozici ke koupi prostřednictvím O'Reilly a dalších prodejců, jako je Amazon. Pokud se vám nějaká část tohoto příspěvku líbí, podívejte se na Typy a gramatika pro mnohem podrobnější pokrytí typů JS, donucování a gramatických pravidel.

Upozornění: Zde leží kontroverzní,a opravdu dlouhá , diskuze, která vás pravděpodobně rozruší. Tento příspěvek obhajuje a podporuje často nenáviděný nátlak v JavaScriptu mechanismus. Všechno, co jste kdy slyšeli nebo cítili o tom, co je špatného na nátlaku, bude zpochybněno tím, co zde nastíním. Ujistěte se, že jste si na tento článek vyhradili dostatek času.

Není Coercion již mrtvý?

Proč proboha mluvím o – natož o obhajování a schvalování! – mechanismu, který byl tak všeobecně hodnocen jako příšerný, zlý, magický, bugový a špatný jazykový design? Copak ta loď už dávno nevyplula? Nepohnuli jsme se všichni dál a nenechali jsme nátlak v prachu? Pokud Crockford říká, že je to špatné, pak to tak musí být.

Ummm... ne. Naopak si myslím, že nátlaku se nikdy nedostalo fér, protože se o něm nikdy nemluvilo ani se o něm správně neučilo. Není divu, že nenávidíte nátlak, když vše, co jste kdy viděli, je úplně špatný způsob, jak tomu porozumět.

Ať už je to dobré nebo špatné, téměř celý důvod pro Typy a gramatiku kniha, stejně jako mnoho mých konferenčních přednášek, je právě tento případ.

Ale možná ztrácím čas snahou vás přesvědčit. Možná nikdy nezměníte názor.

Ve skutečnosti má pan Crockford co říci přímo k tomuto bodu:

Douglas Crockford - Jsou pryč? "The Better Parts", Nordic.js 2014

Takže, má pravdu? Ještě výstižnější je, že nátlak by mohl být „dalším nápadem“, který prostě nemá dost staré generace statického psaní vymře dostat spravedlivé a objektivní vyšetření?

Myslím, že možná ano.

V podstatě se už roky rozhlížím po odpůrcích nátlaku a ptám se:„Už jsou pryč?“

Nepohodlná pravda

"Dělejte, jak říkám, ne jak dělám."

Rodiče ti to říkali, když jsi byl dítě, a tenkrát tě to štvalo, že? Vsadím se, že by vás dnes naštvalo, kdyby někdo z naší profese zastával tento postoj.

Když tedy slyšíte Douglase Crockforda mluvit negativně o nátlaku, jistě předpokládáte, že se podobně vyhýbá jeho použití ve svém vlastním kódu. Že jo? Ummm... jak to mám napsat? Jak vám to mám prolomit?

Crockford používá nátlak. Řekl jsem to. Nevěříte mi?

// L292 - 294 of json2.js
for (i = 0; i < length; i += 1) {
    partial[i] = str(i, value) || 'null';
}

json2.js, L293

Vidíte ten nátlak? str(..) || 'null' . Jak to funguje?

Pro || operátor, první operand (str(..) ) je implicitně vynucen na boolean pokud již není jeden, a to true / false hodnota se pak použije pro výběr buď prvního operandu (str(..) ) nebo druhý ('null' ). Přečtěte si více o tom, jak || a && práce a běžné idiomatické použití těchto operátorů.

V tomto případě se rozhodně neočekává, že první operand bude boolean , jak dříve dokumentuje str(..) fungovat takto:

function str(key, holder) {

// Produce a string from holder[key].

..

Jeho vlastní kód tedy spoléhá na implicitní nátlak zde. Už to, co nám deset let přednášel, je špatné. A proč? Proč to používá?

Ještě důležitější je, proč vy používat takové idiomy? Protože vím, že ano. Většina vývojářů JS používá toto || operátor idiom pro nastavení výchozí hodnoty proměnné. Je to super užitečné.

Místo toho mohl ten kód napsat takto:

tmp = str(i, value);
partial[i] = (tmp !== '') ? tmp : 'null';

Tím se zcela vyhýbá nátlaku. !== operátor (ve skutečnosti všechny operátory rovnosti, včetně == a != ) vždy vrátí boolean z kontroly rovnosti. ? : operátor nejprve zkontroluje první operand a poté vybere buď druhý (tmp ) nebo třetí ('null' ). Žádný nátlak.

Tak proč to neudělá?

Protože str(..) || 'null' idiom je běžný, kratší/jednodušší na zápis (není potřeba dočasné proměnné tmp ), a obecně snadno pochopitelné, jistě ve srovnání s formou bez donucení.

Jinými slovy, nátlak, zejména implicitní donucení, má použití, kde ve skutečnosti zlepšuje čitelnost našeho kódu.

Dobře, takže to je jen jedna izolovaná výjimka, kterou udělal, že? Ne tak docela.

Pouze v jednom souboru „json2.js“ je zde (ne nutně úplný) seznam míst, která Crockford používá buď explicitní nebo implicitní donucení:L234, L275, L293, L301-302, L310, L316-317, L328-329, L340-341, L391, L422 a L442.

OH Počkej. Toto je pouze stará knihovna "json2.js". To je nespravedlivé, že? Co říkáte na jeho vlastní knihovnu JSLint, kterou stále spravuje (EDIT:brzy vydá aktualizaci pro ES6):L671, L675, L713, L724, L782, ... Chápete to, ne?

Doug Crockford používá nátlak, aby byl jeho kód čitelnější. Tleskám mu za to.

Ignorujte, co říká o tom, že nátlak je zlý nebo špatný. Je to užitečné a on dokazuje, že se svým kódem bez ohledu na to, jaké slidy, které zaujmou titulky, umístí ve svých konferenčních projevech.

Ale... == Is The Evil

OK, máte pravdu, neexistuje jediný výskyt == v jeho kódu. A kdykoli se vysmívá nátlaku, téměř jistě mluví o == konkrétně.

Takže jsem nespravedlivý, když zdůrazňuji spoustu jiných než == nátlaky? Vlastně bych tvrdil, že je to on, kdo je nespravedlivý tím, že neustále rovná == s nátlakem (samozřejmě zamýšlená slovní hříčka!). Není sám. Řekl bych, že téměř všichni vývojáři JS dělají totéž. Když slyší "nátlak", nevyhnutelně vyvolají == .

Nátlak je mechanismus, který může fungovat při == se používá a nelze jej použít při === se používá. Ale z této realizace by mělo být jasné, že == a donucení jsou ortogonální zájmy. Jinými slovy, můžete si stěžovat na == které jsou oddělené od stížností na samotný nátlak.

Nesnažím se tady jen hnidopišit. To je velmi důležité pro pochopení zbytku tohoto příspěvku:donucení musíme zvažovat odděleně od zvažování == . Zavolejte na číslo == „donucování k rovnoprávnosti“, chcete-li, ale neslučujte to pouze s donucováním samotným.

Celkově si myslím, že téměř všechny stížnosti byly proti == jsou ve skutečnosti problémy s nátlakem a k těm se dostaneme později. Vrátíme se také k == a podívejte se na to trochu víc. Pokračujte ve čtení!

Potřebujete nátlak?

Nátlak je to, co se děje v JavaScriptu, když potřebujete přejít od jednoho typu (například string ) na jiný (například boolean ). To však není jedinečné pro JS. Každý programovací jazyk má hodnoty různých typů a většina programů vyžaduje převod z jednoho na druhý. V jazycích se staticky typovaným (vynuceným typem) se konverze často nazývá „casting“ a je explicitní. Ke konverzi však přesto dojde.

Nátlak JavaScriptu může být buď úmyslný, nebo explicitní nebo se to může stát implicitně jako vedlejší účinek.

Ale jen stěží existují nějaké netriviální JS programy, které by se v té či oné chvíli nespoléhaly na nátlak nějaké formy. Když lidé nenávidí z donucení, obvykle nenávidí z implicitního donucení, ale explicitní donucení je obvykle považováno za OK.

var x = 42;

var y = x + "";     // implicit coercion!
y;                  // "42"

var z = String(x);  // explicit coercion!
z;                  // "42"

I pro ty, kteří jsou veřejně proti implicitnímu donucení, z nějakého důvodu jim obvykle vyhovuje x + "" formulář zde. Upřímně nechápu, proč toto implicitní donucení je v pořádku a mnoho dalších ne.

Můžete se zaměřit na rozhodnutí, zda dáváte přednost explicitnímu nebo implicitní donucovací formy, ale nemůžete rozumně tvrdit, že většinu programů JS lze napsat zcela bez donucení.

Strašně mnoho vývojářů říká, že bychom neměli mít nátlak, ale téměř nikdy si nedají čas na to, aby promysleli všechny možné případy. Nemůžete prostě říct, že by mechanismus neměl existovat, aniž byste měli odpověď na to, co byste místo toho měli udělat.

Tento článek je v jistém smyslu cvičením v tomto úsilí, aby prozkoumal, jak rozumný je takový postoj. Nápověda:nic moc.

Proč donucení?

Případ nátlaku je mnohem širší, než zde plně popíšu. Podívejte se na kapitolu 4 v části Typy a gramatika pro mnohem více podrobností , ale pokusím se stručně navázat na to, co jsme viděli dříve.

Kromě x || y (a x && y ) idiomy, které mohou být docela užitečné při vyjadřování logiky jednodušším způsobem než x ? x : y ve formě, existují další případy, kdy donucení, dokonce implicitní donucení, je užitečné pro zlepšení čitelnosti a srozumitelnosti našeho kódu.

// no coercion
if (x === 3 || x === "3") {
    // do something
}

// explicit coercion
if (Number(x) == 3) {
    // do something
}

// implicit coercion
if (x == 3) {
    // do something
}

První forma podmíněné sukně nátlaku zcela. Ale je to také delší a "složitější" a řekl bych, že zde zavádí další podrobnosti, které mohou být velmi dobře zbytečné.

Pokud je záměrem tohoto kódu něco udělat pokud x je tři hodnotu, bez ohledu na to, zda je v jeho string formulář nebo number formulář, skutečně potřebujeme znát tento detail a přemýšlet o něm zde? Trochu záleží.

Často ne. Často bude tato skutečnost detailem implementace, který byl abstrahován do x dostal sadu (z prvku formuláře webové stránky nebo odpovědi JSON nebo ...). Měli bychom to nechat abstrahované a použít určitý nátlak ke zjednodušení tohoto kódu tím, že budeme tuto abstrakci podporovat.

Tedy Number(x) == 3 lepší nebo horší než x == 3 ? V tomto velmi omezeném případě bych řekl, že jde o přehození. Nehádal bych se s těmi, kteří preferují explicitní formulář přes implicitní . Ale líbí se mi to implicitní formulář zde.

Zde je další příklad, který se mi líbí ještě více:

// no coercion
if (x === undefined || x === null) {
    // do something
}

// implicit coercion
if (x == null) {
    // do something
}

implicitní formulář zde funguje, protože specifikace říká, že null a undefined jsou z donucení rovnocenní k sobě navzájem a k žádným jiným hodnotám v jazyce. To znamená, že je naprosto bezpečné léčit undefined a null jako k nerozeznání a skutečně bych jej důrazně doporučil to.

x == null test je zcela bezpečný před jakoukoli jinou hodnotou, která může být v x vynucení na null , garantováno spec. Proč tedy nepoužít kratší formu, abychom odstranili tento podivný detail implementace obou undefined a null prázdné hodnoty ?

Pomocí === vám brání využívat všech výhod nátlaku. A bylo vám řečeno, že to je odpověď na všechny problémy s nátlakem, že?

Zde je špinavé tajemství:< , <= , > a >= porovnávací operátory a také + , - , * a / matematické operátory, nemají žádný způsob, jak zakázat donucení. Takže jednoduše pomocí === ani vzdáleně neopraví vše vaše problémy, ale odstraní skutečně užitečné případy donucovací rovnosti == nástroj.

Pokud nesnášíte nátlak, stále se musíte potýkat se všemi místy, kde je === nemůžu ti pomoci. Nebo můžete přijmout a naučit se používat nátlak ve svůj prospěch, takže == pomáhá vám místo toho, aby vám dával záchvaty.

Tento příspěvek má mnohem více, k čemu se dostat, takže se nebudu dále rozepisovat pro donucení a == . Opět kapitola 4, Typy a gramatika pokrývá téma mnohem podrobněji, pokud vás to zajímá.

Příběh dvou hodnot

Právě jsem vychvaloval, proč je donucování tak skvělé. Ale všichni víme, že nátlak má některé ošklivé části – nelze to popřít. Pojďme k bolesti, která je vlastně smyslem tohoto článku.

Udělám možná pochybné tvrzení:kořenem většiny zla v nátlaku je Number("") výsledkem je 0 .

Možná budete překvapeni, kolik dalších případů nátlaku se shodne s tímto jedním. Jo, jo, jsou i další. Dostaneme se tam.

Řekl jsem to dříve, ale stojí za to to zopakovat:všechny jazyky se musí vypořádat s převody typů, a proto se všechny jazyky musí vypořádat s rohovými případy, které vedou k podivným výsledkům. Každý jeden.

// C
char s[] = "";
int num = atoi(s);
printf("%d",num);                   // 0

// Java
String s = "";
Integer num = Integer.valueOf(s);
System.out.println(num);            // java.lang.NumberFormatException

C se rozhodne převést "" na 0 . Java si ale stěžuje a hodí výjimku. JavaScript zjevně není touto otázkou výlučně sužován.

Ať už je to dobré nebo špatné, JavaScript musel činit rozhodnutí pro všechny tyto druhy rohových případů a upřímně řečeno, některá z těchto rozhodnutí jsou skutečné zdroj našich současných potíží.

Ale v těchto rozhodnutích byla nepopiratelná – a myslím, že obdivuhodná – filozofie designu. Alespoň v začátcích se JS rozhodl odklonit od filozofie „pokaždé, když uděláte něco divného, ​​hodíme výjimku“, kterou získáte z jazyků jako Java. To je způsob myšlení „odpadky dovnitř, odpadky ven“.

Jednoduše řečeno, JS se snaží co nejlépe odhadnout, o co jste ho požádali. Chybu hází pouze v krajních případech, kdy nemohl přijít na žádné rozumné chování. A mnoho dalších jazyků zvolilo podobné cesty. JS je spíše jako „odpadky dovnitř, nějaké recyklované materiály ven“.

Když tedy JS zvažoval, co dělat s řetězci jako "" , " " a "\n\n" když byl požádán, aby je přinutil k číslu, zvolil zhruba:trim all whitespace; pokud pouze "" je vlevo, vraťte 0 . JS nevyvolává výjimky všude, a proto dnes většina kódu JS nepotřebuje try..catch obtéká téměř každý jednotlivý výrok. Myslím, že to byl dobrý směr. To může být hlavní důvod, proč mám JS rád.

Zvažme tedy:je rozumné pro "" stát se 0 ? Liší se vaše odpověď pro " " nebo "\n\n" ? Pokud ano, proč, přesně? Je divné, že obě "" a "0" přinutit ke stejnému 0 číslo? Eh Zdá se mi to jako ryba.

Dovolte mi položit obrácenou otázku:bylo by rozumné pro String(0) vytvořit "" ? Samozřejmě ne, jasně bychom očekávali "0" tam. Hmmm.

Jaká jsou ale další možná chování? Mělo by Number("") vyvolat výjimku (jako Java)? Uf, ne. To neúnosně porušuje filozofii designu. Jediné další rozumné chování, které si dokážu představit, je vrátit NaN .

NaN by nemělo být považováno za „ne číslo“; nejpřesněji je to stav neplatného čísla. Obvykle dostanete NaN z provádění matematické operace, aniž by požadované hodnoty byly čísla (nebo podobné číslům), například 42 / "abc" . Symetrické uvažování z donucení dokonale sedí:cokoli, co se snažíte donutit k číslu, které není jasně platná reprezentace čísla by měla vyústit v neplatné číslo NaN —skutečně Number("I like maths") vytváří NaN .

Pevně ​​věřím Number("") měl by výsledkem je NaN .

Coercing "" na NaN ?

Co kdybychom mohli změnit pouze tuto jednu věc na JavaScriptu?

Jednou z běžných donucovacích rovností, která vytváří zmatek, je 0 == "" rovnost. A Hádej co? Vychází přímo ze skutečnosti, že == algoritmus v tomto případě říká pro "" stát se číslem (0 již je jedna), takže skončí jako 0 == 0 , což je samozřejmě true .

Pokud tedy "" místo toho vynucen na NaN číselná hodnota namísto 0 , kontrola rovnosti by byla 0 == NaN , což je samozřejmě false (protože nic se nikdy nevyrovná NaN , dokonce ani sám sebe!).

Zde můžete vidět základ mé celkové teze:problém s 0 == "" není == sama o sobě – její chování je alespoň v tomto případě docela rozumné. Ne, problém je s Number("") samotný nátlak. Pomocí === vyhnout se těmto případům je jako dát si na čelo obvaz k léčbě bolesti hlavy.

Jen léčíte symptom (i když špatně!), neřešíte problém. Problémem je vynucování hodnoty. Takže problém vyřešte. Nechte == sám.

Blázen, říkáš? Number("") nelze nijak opravit produkující 0 . Máte pravdu, objevilo by se to neexistuje způsob, jak to udělat, ne bez přerušení milionů programů JavaScript. Mám nápad, ale k tomu se vrátíme později. Musíme toho ještě hodně prozkoumat, abychom pochopili mou širší pointu.

Pole na řetězec

Co třeba 0 == [] ? To vypadá divně, že? To jsou jasně jiné hodnoty. A i kdybyste si mysleli pravdu/nepravdu, [] by měla být pravdivá a 0 by měl být falešný. Takže, WTF?

== Algoritmus říká, že pokud jsou oba operandy objekty (objekty, pole, funkce atd.), stačí provést referenční srovnání. [] == [] vždy selže, protože se vždy jedná o dva různé odkazy na pole. Ale pokud jeden z operandů není objekt, ale místo toho je primitivní, == se snaží udělat z obou stran primitiva a skutečně primitiva stejného typu.

Jinými slovy, == preferuje porovnávání hodnot stejného typu. To je docela rozumné, řekl bych, protože dávat rovnítko mezi hodnoty různých typů je nesmysl. I my vývojáři máme ten instinkt, že? Jablka a pomeranče a všechen ten jazz.

Takže [] se musí stát primitivem. [] se ve výchozím nastavení stává řetězcovým primitivem, protože nemá žádné výchozí donucování k číslování. Jakým řetězcem se to stane? Zde je další nátlak, o kterém bych tvrdil, že je zničen původním designem:String([]) je "" .

Z nějakého důvodu je výchozím chováním polí to, že se stringují pouze do řetězcové reprezentace jejich obsahu. Pokud nemají žádný obsah, zůstane pouze "" . Samozřejmě je to složitější, protože null a undefined , pokud je přítomen v hodnotách pole, také reprezentovat jako "" spíše než mnohem rozumnější "null" a "undefined" očekávali bychom.

Stačí říci, že stringifikace polí je docela zvláštní. čemu bych dal přednost? String([]) by mělo být "[]" . A mimochodem, String([1,2,3]) by mělo být "[1,2,3]" , nejen "1,2,3" jako aktuální chování.

Takže zpět k 0 == [] . Stane se 0 == "" , který jsme již označili jako nefunkční a vyžadující opravu. Pokud buď String([]) nebo Number("") (nebo obojí!) byly opraveny, šílenství, které je 0 == [] odešla by pryč. Stejně jako 0 == [0] a 0 == ["0"] a tak dále.

Znovu:== není problém, stringifikace polí ano. Opravte problém, ne symptom. Nechte == sám.

Poznámka: Podivná je i stringifikace objektů. String({ a: 42 }) vytváří "[object Object]" kupodivu, když {a:42} by dávalo mnohem větší smysl. Nebudeme se zde více ponořit do tohoto případu, protože to obvykle není spojeno s problémy s donucováním. Ale přesto je to WTF.

Další Gotchas (které nejsou == Chyba)

Pokud nerozumíte == kroky algoritmu, myslím, že by bylo dobré si je několikrát přečíst, abyste je obeznámili. Myslím, že budete překvapeni, jak rozumné == je.

Jedním z důležitých bodů je, že == porovnávání řetězců provádí pouze tehdy, jsou-li obě strany buď již řetězci, nebo se stávají řetězci z objektu přecházejícího do primitiva. Takže 42 == "42" může mít dojem, že se s ním zachází jako s "42" == "42" , ale ve skutečnosti je považován za 42 == 42 .

Stejně jako když vám učitel matematiky vyčítal, že jste dostali správnou odpověď ze špatného důvodu, neměli byste se spokojit s náhodným předpovídáním == chování, ale místo toho se ujistěte, že rozumíte tomu, co ve skutečnosti dělá.

A co mnoho dalších běžně citovaných == máš?

  • false == "" :Na tohle si nebude tolik z vás stěžovat. Oba jsou falešní, takže je to alespoň v sousedství rozumného. Ale ve skutečnosti je jejich nepravda irelevantní. Oba se stanou čísly, 0 hodnota. Už jsme ukázali, co je potřeba změnit.

  • false == [] :Co? [] je pravda, jak to může být == false ? Zde budete pravděpodobně v pokušení přemýšlet o [] by měl být vynucen na true / false , ale není. Místo toho false se stane číslem (0 přirozeně), a tak je to 0 == [] a právě jsme tento případ viděli v předchozí části.

    Měli bychom změnit Number(false) z 0 do NaN (a symetricky Number(true) do NaN )? Určitě, pokud měníme Number("") do NaN , mohl bych udělat ten případ. Zejména proto, že můžeme pozorovat Number(undefined) je NaN , Number({}) je NaN a Number(function(){}) je NaN . Může zde být důležitější konzistence?

    Nebo ne. Silná tradice z jazyka C je pro false do 0 a naopak Boolean(0) jasně by mělo být false . Hádám, že tohle je přehoz.

    Ale tak či tak, false == [] by bylo opraveno, pokud by byly opraveny jiné dříve uvedené problémy se stringováním pole nebo prázdnými řetězci!

  • [] == ![] :Oříšky! Jak se může něco rovnat negaci sebe sama?

    Bohužel, to je špatná otázka. ! dojde před == se dokonce uvažuje. ! vynutí boolean donucení (a převrátí svou paritu), takže ![] se změní na false . Tento případ je tedy pouze [] == false , kterému jsme se právě věnovali.

Kořen všeho == Zlo

Dobře Počkej. Pojďme se na chvíli podívat.

Právě jsme prošli hromadou běžně citovaných == WTF. Můžete pokračovat v hledání ještě více == podivnost, ale je dost pravděpodobné, že byste se dostali zpět k jednomu z těchto případů, které jsme právě citovali, nebo k nějaké jeho variantě.

Ale jedna věc vše tyto případy mají společné to, že Number("") byl změněn na NaN , všichni by kouzelně být opraveno. Všechno se vrací na 0 == "" !!

Volitelně můžeme také opravit String([]) na "[]" a Number(false) na NaN , pro dobrou míru. Nebo ne. Mohli bychom opravit 0 == "" . Ano, říkám, že prakticky všechny frustrace kolem == vlastně pramení z toho jednoho rohového pouzdra , a navíc v podstatě nemají téměř nic společného s == sám.

Zhluboka se nadechněte a nechte to vniknout.

Přidávání Našim frustracím

Opravdu bych si přál, abych zde mohl článek ukončit. Ale není to tak jednoduché. Ano, opravuji Number("") opravuje téměř vše z == běda, ale == je pouze jedním z mnoha míst, kde lidé v JS zakopnou o nátlak.

Další nejčastější zdroj problémů s nátlakem přichází při použití + operátor. Znovu uvidíme, že stížnosti jsou obvykle vzneseny proti + , ale ve skutečnosti jsou na vině základní hodnoty nátlaku.

Někomu přetěžování + docela vadí být jak matematické sčítání, tak zřetězení řetězců. Abych byl upřímný, ani miluji, ani nenávidím tuto skutečnost. Mně to jde, ale taky bych byl docela OK, kdybychom měli jiného operátora. Bohužel nemáme a pravděpodobně nikdy nebudeme.

Jednoduše řečeno, + provádí zřetězení řetězců, pokud je některý z operandů řetězec. Jinak dodatek. Pokud + se používá s jedním nebo oběma operandy, které tomuto pravidlu nevyhovují, jsou implicitně vynucen, aby odpovídal očekávanému typu (buď string nebo number ).

Na první pohled by se mohlo zdát, že pro nic jiného, ​​než pro konzistenci s == , tedy + by se měl zřetězit pouze v případě, že oba již byly řetězce (bez nátlaku). A v rozšíření by se dalo říci, že sčítá pouze v případě, že oba operandy již byly čísla (bez donucení).

Ale i kdybychom udělali změnit + takhle by to neřešilo rohové případy smíchání dvou různých typů s + :

42 + "";    // "42" or 42?
41 + "1";   // "411" or 42?

Co by mělo být + dělat tady? Vyhození chyby je tak Java. 1994 právě volali.

Je sčítání skutečně lepší než zřetězení zde, nebo naopak? Můj odhad je, že většina lidí dává přednost zřetězení ("42" ) pro první operaci, ale sčítání (42 ) za druhé. Nejednotnost tohoto postoje je však hloupá. Jediný rozumný postoj je, že výsledkem obou těchto operací musí být "42" a "411" (jako aktuálně) nebo 42 a 42 (jak se předpokládá).

Ve skutečnosti, jak jsem tvrdil dříve, pokud je první + Kromě toho by tato operace měla vyústit v NaN , nikoli 42 , jako "" musí být NaN místo 0 . Stále byste preferovali NaN / 42 na "42" / "411" , pak? Pochybuji.

Nemyslím si, že existuje lepší chování, které bychom mohli změnit + do.

Jak tedy vysvětlíme + gotchas, pokud to není + chyba operátora? Stejně jako předtím:važte si nátlaku!

Například:

null + 1;           // 1
undefined + 1;      // NaN

Než vysvětlím, který z těch dvou se zdá rozumnější? Bez výhrad bych řekl, že to druhé je mnohem rozumnější než to první. Ani null ani undefined jsou čísla (ani řetězce), takže + nelze pravděpodobně považovat za platnou operaci s nimi.

Ve dvou výše uvedených + operace, žádný z operandů není řetězec, takže jsou oba číselné sčítání. Dále vidíme, že Number(null) je 0 ale Number(undefined) je NaN . Jeden z nich bychom měli opravit, aby byly alespoň konzistentní, ale které?

Jsem pevně přesvědčen, že bychom měli změnit Number(null) být NaN .

Další nátlakové WTF

Již jsme zdůraznili většinu případů, se kterými se pravděpodobně setkáte při každodenním kódování JS. Dokonce jsme se pustili do některých bláznivých rohových pouzder, které jsou populárně citované, ale na které většina vývojářů narazí jen zřídka.

Ale v zájmu vyčerpávající úplnosti jsem sestavil obrovskou drsnou tabulku celé hromady různých hodnot a všech implicitních a explicitních nátlaků, kterými je můžete provést. Popadněte silnou láhev alkoholu (nebo svůj oblíbený zvládací mechanismus) a ponořte se do toho.

Pokud hledáte případ kritizovat donucení, téměř jistě jej (nebo jeho kořen) na tomto seznamu najdete. V této tabulce se skrývá několik dalších překvapení, ale my jsme probrali ta, kterých se musíte obávat.

Můžeme to opravit?

Dlouze jsem blábolil jak o tom, proč je donucení úžasné, tak o tom, proč má problémy. Je důležité si uvědomit, že z mého pohledu nejsou operátoři na vině, i když se jim dostává veškeré negativní pozornosti.

Skutečná vina spočívá v některých pravidlech vynucování hodnot. Ve skutečnosti je kořenový seznam problémů poměrně krátký. Pokud je opravíme, budou se postupně opravovat spoustu dalších non-root problémů, které podrazí vývojáře.

Pojďme si zrekapitulovat základní problémové hodnoty nátlaků, které nás znepokojují:

  • Number("") je 0

    Mělo by být: NaN (opravuje většinu problémů!)

  • String([]) je "" , String([null]) je "" , String([undefined]) je ""

    Mělo by být: "[]" , "[null]" , "[undefined]"

  • Number(false) je 0 , Number(true) je 1

    Mělo by být (volitelné/diskutovatelné): NaN , NaN

  • Number(null) je 0

    Mělo by být: NaN

Dobře, takže co můžeme udělat pro nápravu těchto problémů (nátlak na hodnotu) místo léčby symptomů (operátoři)?

Přiznám se, že neexistuje žádná kouzelná kulka, kterou bych mohl vytáhnout. Neexistuje žádný trik (no... mohli bychom monkey-patch Array.prototype.toString() opravit tyto případy). Neexistuje žádný hluboký vhled.

Ne, abychom to napravili, budeme to muset použít hrubou silou.

Navrhnout TC39 přímou změnu kteréhokoli z nich by v prvním kroku selhalo. Je doslova nulová šance, že takový návrh uspěje. Ale je tu jiný způsob, jak zavést tyto změny, a to by mohlo, jen možná, mít nepatrný zlomek % šance. Pravděpodobně nula, ale možná je to jako 1e-9.

"use proper";

Tady je můj nápad. Pojďme si představit nový režim, který zapíná "use proper"; pragma (symetricky k "use strict" , "use asm" , atd.), což změní tyto hodnotové nátlaky na jejich správné chování.

Například:

function foo(x) {
    "use proper";

    return x == 0;
}

foo("");    // false
foo([]);    // false
foo(false); // false

foo("0");   // true

Chápete, proč je to jiné – a já tvrdím, že lepší – než === ? Protože stále můžeme použít == pro bezpečné nátlaky jako "0" == 0 , o kterém by drtivá většina z nás řekla, že je to stále rozumné chování.

Kromě toho by všechny tyto opravy byly v platnosti:

"use proper";

Number("");             // NaN
Number("  ");           // NaN
Number("\n\n");         // NaN
Number(true);           // NaN
Number(false);          // NaN
Number(null);           // NaN
Number([]);             // NaN

String([]);             // "[]"
String([null]);         // "[null]"
String([undefined]);    // "[undefined]"

0 == false;             // false
1 == true;              // false
-1 < "";                // false

1 * "";                 // NaN
1 + null;               // NaN

You could still use === to totally disable all coercions, but "use proper" would make sure that all these pesky value coercions that have been plaguing your == and + operations are fixed, so you'd be free to use == without all the worry!

What Next?

The theoretical proposal I've just made, which likely has near zero chance of ever getting adopted even if I did formally propose it, doesn't seem like it leaves you with much practical take away from all this reading. But if enough of you latch onto the ideas here and help create momentum, it might have a remote chance.

But let me suggest a couple other possibilities, besides the standards track, to chew on:

  1. "use proper" could be become a new transpile-to-JavaScript language ("ProperScript", "CoercionScript", etc), in the same spirit as TypeScript , Dart , SoundScript , etc. It could be a tool that transforms code by wrapping all value operations in runtime checks that enforce the new rules. We could lessen the obvious performance hit quite a bit by specifying annotations (again, TypeScript style) that hint the tool which operations it should wrap.
  2. We could take these sets of desired new value coercion rules and turn them into assertions for a build-process that does simulated run-time checks (with test data) to "lint" your code, in a similar spirit to the RestrictMode project, one of my favorite sleeper projects. This tool would spit out warnings if it detects places in your code that expect coercion results that don't hold.

Awareness

Finally, let me just say that even if none of this proposal ever comes to pass, I believe there's still value to be gleaned from this article. By learning exactly what things are going wrong in your == and + operations—that is, the value coercion corner cases themselves—you're now empowered to write better, more robust code that robustly handles (or at least avoids) these cases.

I believe it's far healthier to be aware of the ins and outs of coercion, and use == and === responsibly and intentionally, than it is to just use === because it's easier not to think and not to learn.

If you take writing JS seriously, and I hope you do, isn't it worth your time to internalize this discipline? Won't that do more to improve your code than any blindly-applied linting rule ever will?

Don't forget to check out my You Don't Know JS book series, and specifically the YDKJS:Types &Grammar title, which can be read for free online or purchased through O'Reilly and other sellers.