Co se mi nelíbí na JavaScriptu

Když jsem v 90. letech s JavaScriptem začínal, tak jsem ten jazyk nesnášel. Hlavní důvod byl ale ten, že co jsem napsal pro jeden prohlížeč, nefungovalo v druhém. Když jsem pochopil, že to není až tak problém jazyka, ale rozdílného API v prohlížečích, tak jsem si JavaScript postupně začal oblibovat, až se stal mým druhým nejoblíbenějším jazykem. Navíc je mnohem elegantněji navržený než PHP a temných zákoutí je v něm mnohem míň.

Na JavaScriptu se mi nejvíc líbí myšlenka, že funkce je úplně obyčejná hodnota podobně jako třeba řetězec – dá se kdekoliv definovat, předávat, není na ní zkrátka nic zvláštního. Většina ostatních jazyků, které znám, považuje funkce za něco speciálního a teprve v poslední době umožňuje funkce vytvářet a používat na místech, kde se dá pracovat s jakýmikoliv jinými hodnotami.

Část věcí, které se mi na JavaScriptu nelíbí, je nejspíš dána tím, že byl navržen za 10 dní před téměř 20 lety a od té doby se prakticky nezměnil. Část je ale také dost možná jiná filozofie.

Nutnost psát var

Pro vytvoření lokální proměnné je potřeba použít klíčové slovo var. Na tom by nebylo nic až tak špatného, pokud by jeho neuvedení neznamenalo, že pracujete s globální proměnnou. Chování v PHP, kde se defaultně pracuje s lokálními proměnnými a pokud chcete globální, tak musíte použít global, mi přijde mnohem lepší.

Řekl bych, že to je do značné míry vynuceno vlastností, že funkce jsou normální hodnoty. Když v JavaScriptu voláme funkci f, musí se najít proměnná f a zkusit se zavolat hodnota, která je v ní uložená. Pokud by práce s globálními proměnnými byla explicitní, tak bych vždy musel uvést, že chci volat globální f a ne tu lokální.

Function scope proměnných

Block scope je mnohem přirozenější a příjemnější na používání než function scope, kromě toho vede k méně chybám. Navíc v JavaScriptu všechny lokální proměnné vzniknou hned na začátku funkce, takže následující kód je platný:

function () {
	alert(a); // Zobrazí undefined
	var a = 'Ahoj';
}

Pokud navíc v jedné funkci stejnou proměnnou deklarujete vícekrát (např. dva cykly ve tvaru for (var i = 0; ; )), tak vám nástroje jako JSHint vyhubují. Abyste se tomu vyhnuli a abyste zdůraznili okamžik vzniku proměnných, bylo by nejlepší všechna var napsat hned na začátek funkce, to je ale pořádný opruz.

ECMAScript 6 zavádí block scope pomocí klíčového slova let, v prohlížečích ho ale stejně nepůjde dalších několik let používat kvůli kompatibilitě.

Iterace polí

Nejspolehlivější způsob iterace polí je pomocí for cyklu procházejícího od nuly do array.length. Je to zbytečně krkolomné, index prvku mě často vůbec nezajímá, počet prvků taky zjišťovat nechci. Navíc k iterovanému prvku musím přistupovat přes pole a aktuální index. Pokud chci navíc procházet výsledek funkce, musím si ho uložit do dočasné proměnné. Kód pak vypadá nějak takhle:

var tags = getTags();
for (var i = 0; i < tags.length; i++) {
	var tag = tags[i];
	// tady bude kód pracující s tag
}

Oč jednodušší to je v PHP:

foreach (getTags() as $tag) {
    // tady bude kód pracující s $tag
}

V ECMAScript 5.1 jde používat array.forEach, ale ani to se mi kvůli API používajícímu callback nezdá jako nejlepší řešení.

Iterace objektů

Iteraci objektů taky nepovažuji za ideální především proto, že se prochází i uživatelem definované vlastnosti na prototypu. Takže pokud nějaký chytrák definuje třeba Object.prototype.clone, tak bude clone strašit při iteraci všech objektů. Jako obranu byste ve všech iteracích měli používat hasOwnProperty:

for (var key in map) {
	if (map.hasOwnProperty(key)) {
		var value = map[key];
		// tady bude kód pracující s value
	}
}

Sám jsem naštěstí závislost na cizím kódu hackujícím Object.prototype řešit nemusel, takže hasOwnProperty nepoužívám a problém mě tolik netrápí. I když iterace výhradně přes klíče a nutnost hodnotu získat mě taky obtěžuje. Oč jednodušší je PHP verze:

foreach ($map as $value) {
    // tady bude kód pracující s $value
}

Objekt je HashMap, nikoliv LinkedHashMap

Pole v PHP dovoluje přistoupit k prvku podle klíče v konstantním čase a zároveň ručí za pořadí při procházení. V Java terminologii jde o LinkedHashMap. JavaScript kupodivu za pořadí při procházení objektu neručí (jde tedy o HashMap), i když ho prohlížeče až na některé okrajové případy dodržují. Donedávna jsem o téhle záludnosti neměl tušení. Pokud potřebujete pořadí dodržet, můžete si klíče ukládat do pole, které budete používat při iteraci.

Čárka za posledním prvkem

Za posledním prvkem pole nelze psát čárku. Tedy – už ECMAScript 3 to umožňuje, ale Internet Explorer to měl špatně, takže [0,].length všude vrací 1, ale IE<9 vrací 2. Za posledním prvkem objektu jde psát čárku až od ECMAScript 5, kvůli kompatibilnímu režimu prohlížečů to tedy půjde univerzálně používat až za několik let.

Tento nedostatek vadí především u diffů větších polí a objektů formátovaných na více řádek. Kdykoliv na tohle narazím, tak si představuji, jak to mohlo vypadat v květnu 1995: „Hele, parser nějak funguje, zítra to odevzdávám, trailing comma tam doplním v další verzi.“

Nepovinný středník

Automatické vkládání středníku na nahodilá místa také nepovažuji za právě povedenou vlastnost. Co myslíte, že vrátí následující funkce?

function () {
	return
	{
		x: 2
	}
}

Uhodli jste, že undefined? Za return si totiž JavaScript domyslí středník a následující objekt pochopí jako blok s návěštím x a kódem 2 (za kterým si taky domyslí středník).

V praxi se s tímto problémem člověk naštěstí často nesetká, dnes a denně na něj ale narážím prostřednictvím Google JavaScript Style Guide, který kvůli vkládání středníků vyžaduje zalamování dlouhých řádků za operátorem, nikoliv před ním (jak jsem na to zvyklý jinde).

Prototypová dědičnost

Nápad dědit z vytvořených objektů místo ze tříd jsem nikdy nevzal za své. Když už pominu paměť zbytečně alokovanou při vytváření objektu přiřazovaného do prototypu potomka, jak asi nastavím parametry konstruktoru předka, když ty budu znát až v konstruktoru potomka, aha? ECMAScript 5.1 dovoluje vytvořit prázdný objekt předka pomocí Object.create, sám používám goog.inherits nebo obdobu, což do prototypu přiřadí prázdný objekt, jehož prototyp nastaví na prototyp předka.

Přepisování metod objektu

Hrůzou mi vstávají vlasy na hlavě při představě, že zavolám nějakou cizí funkci, a ona mi pod rukama změní metody mého objektu – třeba je vymění za něco jiného. V testech se to může hodit, ale v běžném životě to nepřináší nic dobrého. Navíc nejsem nijak chráněn proti překlepům – když něco přiřadím do this.itme místo this.time, tak se o tom nijak nedozvím.

ECMAScript 5.1 nabízí řešení v podobě Object.freeze a Object.seal, to je ale poměrně krkolomné na používání:

function Square(x) {
	this.x = x;
}
Square.prototype = Object.freeze({
	getArea: function () {
		return this.x * this.x;
	}
});
var square = Object.seal(new Square(2));
console.log(square.getArea());
square.x = 3;
console.log(square.getArea());

Object.freeze způsobí, že vlastnosti nejde přidávat, měnit ani mazat – to chceme typicky u prototypu. Object.seal způsobí, že vlastnosti nejde přidávat a mazat, pořád jdou ale měnit – to chceme typicky u vlastností definovaných v konstruktoru.

Předávání this

Funkce jako first-class citizen miluji, dojem ale poněkud kazí chování this. Jde o to, že když někam předáte funkci, tak její this bude nějaký jiný, typicky globální objekt. To je potřeba zcela ojediněle a případně se to dá triviálně vyřešit předáním onoho objektu jako parametru funkce. Lépe by mi vyhovovalo, když by this bylo nastaveno na objekt, přes který jsme k metodě přistoupili, případně na aktuální this uvnitř closure. V praxi je pak kód zaset spoustou zbytečných Function.bind, které jen nastavují this na this.

S tímto nedostatkem souvisí i to, že vaší metodě může někdo nastavit this na libovolný objekt.

Dvě prázdné hodnoty

V JavaScriptu jsou podle mě úplně zbytečně dvě různé hodnoty, které znamenají „nic“ – null a undefined. Obě bohužel potřebujete, protože undefined mají proměnné, dokud do nich nic nepřiřadíte, a null vrací některé funkce, např. prompt. V Closure anotacích pak lze najít nádhery jako /** @type {?number|undefined} */, kde ? znamená null a undefined znamená undefined.

Ostatní jazyky, které používám, si vystačí s jednou prázdnou hodnotou. I když chápu akademický smysl dvou různých prázdných hodnot, tak v praxi je to k ničemu a jen to otravuje život.

Chybějící operátor list

Občas mi chybí operátor list dovolující přiřadit více proměnných najednou. Ale je to vlastně jen po volání regExp.exec. Většinou absence této konstrukce vede jen k tomu, že funkce vracející více hodnot vrací objekty a ne pole, což je přehlednější. V ECMAScript 6 to řeší destructuring assignment.

Řetězce v apostrofech nebo uvozovkách

Dřív mi vyhovovalo, že hodnoty atributů v HTML, řetězce v PHP, řetězce v JavaScriptu a dokonce i řetězce v MySQL se dají uzavírat jak do uvozovek, tak do apostrofů. Časem mi ale začala nejednotnost vadit víc než to, že občas ušetřím pár zpětných lomítek. V PHP aspoň uvozovky a apostrofy znamenají dvě různé věci, v JavaScriptu bych uvozovky využil na něco jiného – ideálně na regulární výrazy, jejichž uzavírání do lomítek ztěžuje takové to domácí parsování JavaScriptového kódu.

V praxi na kód s uvozovkami naštěstí díky coding style nenarážím.

Callback hell

Krása konceptu předávání funkcí zavolaných po dokončení asynchronní operace se bohužel poněkud rozmělňuje nutností do sebe tyto callbacky hluboko zanořovat. Vede to ke kódu mnohem nepřehlednějšímu, než když je psaný sekvenčně. Sám na to naštěstí tolik nenarážím, protože na klientské straně na sebe asynchronní operace většinou moc často nenavazují. Ale je to jeden z důvodů, proč mě příliš neláká node.js.

Ve Facebooku se dal asynchronní kód psát sekvenčně díky operátoru yield, který bude i v ECMAScriptu 6. Osobně by se mi ale ještě víc líbilo async/await, což je ale hudba vzdálené budoucnosti, pokud vůbec nějaké.

Závěr

JavaScript považuji za dobrý jazyk. Kromě zmíněných hodnot typu funkce se mi líbí třeba i objektové literály nebo fakt, že kód běží v jednom vlákně, i když je celkem běžně asynchronní – kód a uvažování o jeho chování to dramaticky zjednodušuje a žádné výrazné problémy to nezpůsobuje. Perfektní je i to, že || vrátí první truthy prvek. Vím, že některé nedostatky by mi pomohl překlenout CoffeeScript nebo jiný transpiler, na JavaScriptu mi ale vyhovuje hustota informací ve zdrojovém kódu, kterou považuji u CoffeeScriptu za příliš vysokou. Ze stejného důvodu jsem si nikdy neoblíbil Perl – přišel mi příliš nepřehledný, zato kód v PHP jsem chápal, aniž bych se jazyk musel učit. Java je pro mě zase příliš řídká. S některými věcmi pomáhá Closure Library a Closure Compiler, ale milejší by mi bylo, když bych na nich nemusel záviset. TypeScript mi ve srovnání s možnostmi ekosystému Closure přijde jako chudý příbuzný.

Sérié článků „Co se mi nelíbí“ na Javě, na Go, na JavaScriptu nasvědčuje tomu, že hledám jazyk, ve kterém by se mi programovalo stejně pohodlně jako v PHP (nebo pohodlněji). JavaScript tím jazykem, který bych bez přemýšlení použil na cokoliv, bohužel také není.

Autor pracuje jako Software Engineer v týmu Gmail Security. V minulosti se zabýval především PHP, o kterém napsal knihu a podílel se na oficiální dokumentaci. Je autorem nástroje pro správu databáze Adminer. Poznámky si zapisuje na weblog PHP triky.

Komentáře: 44

Přehled komentářů

skybedy A co včil...
Honza Re: A co včil...
Bystroushaak Re: A co včil...
NoxArt Re: A co včil...
mboy a co hack?
Pavel Dvořák Dart
mboy Re: Dart
Pavel Dvorak Re: Dart
Clary CoffeeScript
Ondřej Žára
Jakub Vrána
Ondřej Žára Re:
Ondřej Re:
Ondřej Žára Re:
Ondřej Re:
Jirka Kosek Re:
Ondřej Žára Re:
HonzaMarek Function.bind(this)
Lemming Cimrman
Lumír Balhar A co Python?
zbyso Re: A co Python?
pavel Re: A co Python?
pavel Re: A co Python?
Bystroushaak Re: A co Python?
jiri.vrany Re: A co Python?
petr b Čárka, čárka stolička sněhulám
Míra Re: Čárka, čárka stolička sněhulám
Taky Míra Re: Čárka, čárka stolička sněhulám
Jakub Vrána Re: Čárka, čárka stolička sněhulám
Jan33 Re: Čárka, čárka stolička sněhulám
jehovista Re: Čárka, čárka stolička sněhulám
Jakub Vrána Re: Čárka, čárka stolička sněhulám
pb scala?
Jakub Vrána
satai Re: scala?
lukyer Použití nuly v JS
Míra Podivností v JS
Jakub Vrána Re: Podivností v JS
Míra Re: Podivností v JS
tomkisw Není to tak špatné
flv JS a php
Jarda Re: JS a php
vojtech.dobes Re: Podivností v JS
snehuliaik Neverim vlastnym ociam...
Zdroj: https://www.zdrojak.cz/?p=13930