Mnohé výstřednosti JavaScriptu: Jak šílenství dává smysl

Dostupné v jazycích:

Desperate programmer

Pokud jste někdy něco vyvíjeli pomocí čistého JavaScriptu, pravděpodobně víte, že tento jazyk má několik zvláštností.

Nechci být za hatera, některé věci skutečně dávají smysl, jakmile pochopíte, jak je ECMA-262 implementován a jak některé věci zpracovávají syntaktické a sémantické analyzátory. A buďme upřímní, většina „gotcha“ věcí, se kterými lidé přicházejí, jsou úmyslně vymyšleny tak, aby vypadaly divně. Na druhou stranu ale výsledky, které získáte z některých konkrétních výrazů, jsou na hranici šílenství.

Pojďme se nejprve podívat na jednodušší věci. Pravděpodobně už víte, že v JavaScriptu '3' - 1 se rovná 2, zatímco '3' + 1 se rovná 31. Na tom však není nic divného, + je operátor používaný jak pro kontatenaci řetězců, tak pro sčítání celých čísel; Který z nich se použije je určeno typem prvního operandu. Funguje to stejným způsobem v C#, Javě a některých dalších jazycích, které automaticky převádějí druhý operand na řetězec.

Co by mohlo být považováno za trochu neobvyklé je to, že jelikož - se používá pouze pro odečítání, implicitně převádí první operand na celé číslo. Ale to se nám jazyk jen snaží být nápomocný – možná někdy až příliš, ale od výše uvedeného příkladu se sčítáním se to až tak neliší.

Pojďme k něčemu zajímavějšímu. Co je podle vás výsledkem tohoto výrazu?

	3 > 2 > 1

Pokud si zkusíte tento příklad v Pythonu, dostanete true, ale v JavaScriptu je to false. Proč tomu tak je?

Nuže, oba jazyky interpretují výraz různými způsoby. Python to vidí jako dvě samostatné podmínky, které testují, zda 3 > 2 a 2 > 1. Tuto syntaxi můžete použít pro intervaly a může to být docela užitečné. JavaScript však neumí tento výraz vyhodnotit jinak než jen zleva doprava. Takže testuje, zda 3 > 2. To je true. Pak se pokusí otestovat, zda je true > 1. K tomu udělá jedinou věc, kterou může, a převede true na 1, takže konečná podmínka je ve skutečnosti 1 > 1, což je false.

Pokud chcete interval otestovat v JavaScriptu stejným způsobem, jakým funguje v Pythonu, musíte jej zapsat jako (3 > 2) && (2 > 1), zkrátit to moc nejde.

Dobře, zkusíme něco jiného. Jaký je podle vás výsledek tohoto dalšího výrazu?

	0.2 + 0.1 == 0.3

Každý normální člověk by řekl, že výsledek je pravdivý. Ale není. A víte co? Tohle není chyba JavaScriptu. Zkuste si to v C#, Javě, Pythonu nebo jiném jazyce. Většina z nich vám řekne, že výsledek je nepravdivý.

Proč tomu tak je? No, je to kvůli způsobu, jakým jsou čísla s plovoucí desetinnou čárkou reprezentována a uložena v paměti počítače. Většina programovacích jazyků používá pro reprezentaci čísel s pohyblivou řádovou čárkou standard IEEE 754, který používá binární reprezentaci.

V binární reprezentaci se čísla, která se v našem systému desítkové soustavy zdají jednoduchá (například 0,1), po převodu do binární podoby stanou nekonečně se opakujícími zlomky. To vede k zaokrouhlovacím chybám při provádění aritmetických operací, protože ne všechna čísla mohou být reprezentována přesně binárně.

Výsledkem je, že když provádíte operace jako sčítání, odečítání, násobení nebo dělení s čísly s plovoucí desetinnou čárkou, můžete ve výsledku narazit na malé nepřesnosti kvůli chybám zaokrouhlování. Tyto nepřesnosti se mohou hromadit během více aritmetických operací, což v některých případech vede k neočekávanému chování.

Takže ne, za tohle opravdu nelze vinit JavaScript. Takhle prostě počítače fungují, není to chyba jazyka. Ale víte, co je chyba jazyka? Tato věc.

	parseInt(0.0000005);

Guy staring at a screen

V jakémkoli jiném jazyce dostanete buď 0, nebo nějakou formu výjimky, v závislosti na tom, jakou funkci parsování nebo formu přetypování se pokoušíte použít. Víte, jaký je výsledek v JavaScriptu? Výsledek je 5. A víte, co je ještě divočejší? parseInt(0.5) je 0. I parseInt(0.000005) je 0. Ale parseInt(0.0000005) nebo řekněme parseInt(0.00000000000005) je 5. Divné, co?

Důvodem je to, že parseInt očekává řetězec jako první argument. Co se ale stane, když v JavaScriptu převedete opravdu malé číslo na řetězec? Z nějakého důvodu JavaScript začne používat vědeckou notaci. Takže 0.0000005 je pak ve skutečnosti 5e-7 jako řetězec. A jak funguje parseInt? No jen začne číst čísla a zastaví se na prvním nečíselném znaku. Takže parseInt("42omgWTF") vrátí 42. A uhodli jste správně, parseInt("5e-7") je 5.

Když už počítáme, víte, jaký je výsledek tohoto malého sčítání?

	010 + 03

Ne, není to 13, ve skutečnosti je to 11. Ale přestane to být divné, když si uvědomíte, že čísla začínající nulami jsou interpretována jako osmičkové (base-8) literály. Proto když napíšete 010, je to považováno za osmičkovou reprezentaci čísla 8 a 03 za osmičkovou reprezentaci čísla 3. Počínaje ECMAScriptem 6 máme nyní předpony jako 0b (binární), 0o (osmičková) a 0x (hexadecimální), ale jednoduchá 0 stále funguje z historických důvodů, i když nebude fungovat ve strict mode.

Nechme matematiku za sebou a promluvme si o typech. Jak pravděpodobně víte, typy opravdu nejsou nejsilnější stránkou JavaScriptu. To je důvod, proč byl koneckonců vytvořen TypeScript. Každopádně, víte, co je typeof(null)? V Javě je null literál speciálního typu null, který nemá žádné jméno (takže nemůžete deklarovat proměnnou tohoto typu). V C# je to v podstatě stejné (alespoň před C# 3.0, dnes už oficiálně null typ vůbec nemá, i když je to trochu složitější). V C++11 existuje nullptr, který je typu std::nullptr_t. A Python má None typu NoneType.

Jaký je tedy typ null v JavaScriptu? Je to object.

Než se chopíme pochodní a vidlí, není to tak šílené, jak se na první pohled zdá. Mnoho jazyků interně považuje null za speciální singleton objekt. Ale vždy má svůj vlastní typ (nebo "žádný typ", to je jen sémantika). Ale pouze v JavaScriptu můžete napsat typeof({'wtf': 'is_this'}) == typeof(null) a získat true.

Mimochodem, věděli jste, že typeof(NaN) je number? Z programátorského hlediska to tak trochu dává smysl, ale pořád je docela vtipné, že "něco, co není číslo, je číslo".

Ale pojďme nyní kouknout na skutečně bizarní věci. Jedna z mých nejoblíbenějších šíleností je tato:

	('b' + 'a' + + 'b' + 'a').toUpperCase();

Víte, jaký je výsledný text výše uvedeného výrazu? Možná mi nebudete věřit, ale výsledkem je BANANA (banán). Taková opičárna, co? (I'll let myself out.)

Ne ale vážně. Nedělám si srandu. A bláznivé je, že to dává smysl (opět). Ale aby tento trik fungoval, potřebujete převod na velká písmena, jinak je až příliš zřejmé, co se vlastně děje.

To druhé plus ve výrazu je ve skutečnosti unární operátor pro druhé b, nikoli binární operátor konkatenující „nic“. Takže to, co děláme, je něco jako:

	('b' + 'a' + (+'b') + 'a').toUpperCase();

Už vidíte, co se děje? Unární operátor + se pokouší převést druhé b na číslo, ale nejde to, takže vrací NaN. A to se pak jednoduše převede na text 'NaN', takže dostaneme 'b' + 'a' + 'NaN' + 'a'. A teď chápete, proč jsme potřebovali velká písmena, jinak by výsledek byl 'baNaNa'.

A na velké finále se pojďmě podívat na tuto nádheru, kterou jsem nedávno objevil:

	new RegExp({}).test('mom');
	new RegExp({}).test('dad');

This is JavaScriptJaké jsou podle vás výsledky výše uvedeného kódu? Ať je to cokoli, určitě je to dvakrát stejná hodnota, že?

Překvápko, výsledky jsou true a false, v tomto pořadí. A co se přesně děje? Jednoduše řečeno, konstruktor RegExp očekává, že jeho prvním argumentem bude buď řetězec, nebo jiný RegExp. Protože to není ani jedno, pokouší se argument převést na řetězec. A jaká je řetězcová reprezentace objektu {} (nebo jakéhokoli jiného objektu)?

Pokud jste někdy museli pracovat s JavaScriptem, pravděpodobně jste se setkali se zrůdností ve formě [object Object]. To je návratová hodnota funkce toString, která je implicitně v neprimitivních prototypech. Funkci můžete samozřejmě přepsat ve svých vlastních objektech nebo můžete použít něco jako JSON.stringify. Ale pokud použijete čistou formu převodu toString nad jakýmkoliv objektem, získáte řetězec '[object Object]'.

Výše uvedený kód je tedy doslova jen tohle:

	new RegExp('[object Object]').test('mom');
	new RegExp('[object Object]').test('dad');

Proč tohle celé funguje je fakt, že řetězec '[object Object]' je ve skutečnosti platný regulární výraz a znamená "matchuj jakákoli písmena mezi těmito závorkami". A jak vidíte, "mom" obsahuje písmeno "o", takže to je úspěšná shoda, která má za následek výsledek true. "dad" na druhou stranu neobsahuje žádné písmeno z řetězce "[object Object]", a proto je výsledek false.

Takže ano, dává to smysl. Tak trochu. Z opravdu zkresleného, klinicky šíleného pohledu na věc to všechno dává smysl. A nyní snad vidíte, proč má JavaScript pověst podivného, nepředvídatelného jazyka, ačkoli obvykle existují důvody, proč dostáváme to, co dostáváme.

Každopádně pokud si chcete otestovat své znalosti o podivnostech JavaScriptu, doporučuji tento jednoduchý kvíz s 25 otázkami: https://jsisweird.com

Při prvním pokusu jsem dostal jen 14/25, ale teď už jsem schopen to zmáknout na jedničku. Zvládnete to i vy?

Přidat komentář

Kód jazyka komentáře.

Plain text

  • Nejsou povoleny HTML značky.
  • Řádky a odstavce se zalomí automaticky.
  • Adresy webů a emailů se převedou automaticky na odkazy.
CAPTCHA
Vložte znaky zobrazené na obrázku.
Tato otázka slouží pro ověření, zda jste člověk a pro zabránění automatizovaného spamu.