JavaScript Restart – Hurá na pole

Velkou bolestí JavaScriptu bývalo poměrně málo nativních funkcí pro práci s poli v JavaScriptu, a klávesy “F”, “O” a “R” jsou na klaviaturách našich živitelů značně osahané. Pole je, vzhledem k chudému počtu (2 v ES4) typů použitelných pro vytváření datových struktur, druhým nejužívanějším.
Seriál: JavaScript Restart (4 díly)
- JavaScript Restart – QuerySelector 11. 2. 2015
- JavaScript Restart – Hurá na pole 23. 2. 2015
- JavaScript Restart – Neidentifikovatelný létající objekt 9. 3. 2015
- JavaScript Restart – Restartováno 27. 3. 2015
Nálepky:
Array.forEach
První (a vlastně nejméně zajímavou funkcí) je forEach, a to zejména proto, že nám umožní pouze nahradit “for” cyklus. Takže místo obligátního:
for (var i = 0; i < items.length; i++) {
item = items[0];
//kód
}
můžeme použít toto:
items.forEach(function(item, i) {
//kód
});
Zatím žádný zázrak a navíc malý kámen úrazu, a tím je kontext callback funkce, čili je si třeba ohlídat, aby reference this znamenala to, co chceme my (protože jinak je kontext window). To se dá zařídit druhým argumentem.
items.forEach(function(item, i) {
//kód
}, context);
Array.forEach je ekvivalentem $().each(), ovšem proč standardizátoři Javascriptu nepoužili kratší each, je pro mě záhadou. Možná protože je tu přeci jen jeden rozdíl, a to v počtu parametrů callback funkce. Obsahuje totiž:
- aktuální položku pole
- index
- referenci na procházené pole
jQuery v $().each() předává callbacku pouze první dva parametry. Druhým rozdílem je, že vrací vždy undefined na rozdíl od $().each(), která vždy vrací pole.
Malý příklad použití v praxi – dejme tomu, že chceme pro každý button zaregistrovat stejný handler. Pak kód v jQuery bude vypadat takto:
$('button').click(handler);
a čistém JavaScriptu takto:
document.querySelectorAll('button').forEach(function(element) {
element.addEventListener('click', handler);
});
Uznávám, že proti jQuery je toto poněkud ukecané.
Nemohu se na tomto místě nezmínit o sadistických sklonech některých vývojářů, kteří vymýšlejí vtipné mozkolamy a variace na for cyklus, jako třeba:
for (var i = 0; i < items.length; ++i) {
//kód
}
Čtení takového kódu bolí, a chudák dědic jejich kódu ještě večer usíná s otázkou: “Proboha proč?”
Array.map
Šikovnou funkcí je map, která je právě hojně využívaná v jQuery, zřejmě proto, že se s ní dá nadělat spousta parády.
Array.map na rozdíl od Array.forEach už cosi vrací, a to pole s hodnotami, které vrátíme v callback funkci.
Následující příklad neudělá nic lepšího než kopii pole:
var newItems = items.map(function(item) {
return item;
}, context);
Pojďme se tedy podívat na něco užitečnějšího. Máme pole id elementů a chceme na základě tohoto pole dostat přímo pole elementů.
var ids= [
'#name',
'#lastname',
'#age'
];
var els = ids.map(function(id) {
return document.querySelectAll(id);
});
Array.filter
Array.filter je funkce na první pohled podobná Array.map s tím rozdílem, že vrací položky pole, pro které vyhověla podmínka v callbacku.
Následující kód vrátí nová pole, která nebudou obsahovat undefined hodnoty z pole items:
var newItems = items.filter(function(item) {
return item !== undefined;
}, context);
Následující příklad slouží k odstranění duplicitních hodnot z pole (nechal jsem se inspirovat tímto příkladem):
var uniqueArray = duplicates.filter(function(elem, pos, duplicatesArray) {
return duplicatesArray.indexOf(elem) == pos;
});
Šikovné, že?
Další užitečné využití funkce Array.filter je při hledání v poli objektů. Chceme-li například dostat seznam všech objektů, které jsou pouze pro čtení, provedeme to takhle:
var objectList = [
{readonly: false},
{readonly: true},
{readonly: true}
];
var readonlyItems = objectList.filter(function(item) {
return item.readonly;
});
Array.reduce
Array.reduce je prazvláštní funkce, jejíž dokumentaci jsem musel přečíst několikrát, než jsem pochopil, co vlastně dělá. Taktéž je zajímavá tím, že momentálně není implementována v jQuery (na rozdíl od filter a map).
Její callback funkce dostává tyto parametry:
- výsledek vrácený při předchozím spuštění callback funkce
- aktuální položku pole
- index
- procházené pole
a právě v první položce je kámen úrazu, jelikož na první pohled to vypadá, že vrací předchozí prvek pole, ale není tomu tak.
Vypadá to nějak takhle:
var newItems = items.reduce(function(previousItem, currentItem) {
//kód
});
Na co tohle použít? Kupříkladu jsem již několikrát hledal elegantní způsob, jak sečíst všechny prvky v poli. S Array.reduce je to hračka:
var sum = itemToSum.reduce(function(previous, current) {
return previous + current;
});
Zde je na místě otázka, jaká hodnota je v prvním parametru previous při prvním průchodu. Je to hodnota první položky, kterou však lze přednastavit jako parametr funkce Array.reduce.
var sum = [1,2,3].reduce(function(previous, current) {
return previous + current;
}, 10);
Výsledek bude tudíž číslo 16.
Array.every
Další z fukcí pole, která vrací jednu hodnotu, je Array.every. Ta nad každou položkou pole provede test, a budou-li všechny vracet true, pak iArray.every vrací true.
Budeme-li chtít vědět, zda jsou všechny objekty (z výše uvedeného příkladu) pouze ke čtení, můžeme použít toto:
var objectList = [
{readonly: true},
{readonly: false},
{readonly: true}
];
var readonly = objectList.every(function(item) {
return item.readonly;
});
Při použítí této funkce je třeba myslet na to, že pokud jednou dostane false, už se dále nepokračuje, čili v tomto příkladě se třetí prvek pole netestuje.
Array.some
A když už máme funkci pro test na všechny položky pole, tak ještě přidáme test pro alespoň jednu. Je to tak, Array.some vrací true, pokud alespoň jeden test vrátí true.
var readonly = [
{readonly: false},
{readonly: true},
{readonly: true}
].every(function(item) {
return item.readonly;
});
A opět je třeba nezapomenout, že cyklus skončí ve chvíli, kdy bude rozhodnuto, tudíž na třetí položku opět nedojde.
Všechny výše zmíněné funkce jsou implementovány v Internet Exploreru 9+, tudíž, pokud nás IE8 netrápí, můžeme je zvesela používat.
Perlička na závěr
K povídání o “polních” funkcích v JavaScriptu přihodím malý trik. Často se stane, že je třeba ověřit funkce validátoru, konkrétně validaci délky, jenže kde honem vzít řetězec např. o délce 255 znaků.
Řešení vypadá takto:
Array(256).join('a');
Proč 256, proč ne 255? Pojďme se podívat na to, co taková konstrukce vlastně dělá.
Array(256);
Vytvoří pole o 256 prvcích undefined. Následně metoda join mezi tyto prvky vloží 255 krát “a”. A máme hotovo.
Další možné řešení je pomocí funkce Array.fill:
Array(255).fill('a').join('');
anebo ještě lépe pomocí String.repeat, což je přesně to, co hledáme:
'a'.repeat(255)
Jenže tyto funkce jsou součástí ES6 a momentálně si je můžete vyzkoušet pouze ve Firefoxu nebo z polyfillem.
Co je spatne na:
?
Vubec nic. Nicmene casto potrebuji iterovat pres prvky pole, ale index me nezajima.
Dokonce si troufám říci, že většinou mě index nezajímá.
Třeba to, že by to mělo být items.length místo length.items
Vzhledem k tomu, ze tak to bylo i v tom uplne prvnim kodu, tak jsem to povazoval proste za preklep a nevydel za nutne do toho ryt.
jo, je videt ze si vazne programatorskej buh, dokazal si pochopit i tak slozitou vec, jako je operator precedence …
Spatne na tom kode je, ze v kazdej iteracii sa zistuje velkost pola co zbytocne spomaluje slucku. ;-)
Nejboj se, že si to JIT nesrovná.
Pravdu mas predsedo. Dnes uz si vlastne runtime .length cachuje. Teraz som si vsimol, ++i. Asi uz starnem.
To snad ne…
To je moje chyba, nevšiml jsem si, že můj markdown konverter je nastaven na psaní uměleckých děl. Sypu si na hlavu.
Hned na první pohled musí byt jasné že,
nebude fungovat a to nejenom kvůli tem uvozovkám, ale hlavně proto že, querySelectorAll nevrací pole, ale NodeList.
Ani slovo o tom ze for je lehce rychlejší než forEach, nebo co je důležitější, že for můžu přerušit kdežto forEach jak už název napovídá je pro všechny prvky.
Musí tam být ještě konverze na pole, ale pak to není příliš vzor čitelnosti.
Něco jako tohle:
Hanba mě fackuje, tohle mi vážně ujelo.
Opraveno jest.
$.each lze přerušit btw taky. Přerušitelný forEach() je vlastně every().
Takže podle tebe je lepší miliarada callbaku než for cyklus, no poťež koště, víc takových jako ty a už abych si obědnal další ram a pár dalších jader
No, každé má sve kouzlo. Minimálně se může hodit nový scope funkce, například.
I když zápis je formou callbacku, o žádný callback se nejedná, jde o synchronní kód, takže tvá obava je lichá. :)
BTW, JS jede v jednom vlákně, takže to cos napsal je samozřejmě úplný blábol.
Funkce reduce neni zadny prapodivny vymysl. Je to jeden ze zakladnich stavebnich kamenu funkcionalniho programovani. To jen, aby si nekdo nemyslel, co to ti standardizatori pridali do Javascriptu za „blbinu“. Naopak – priblizuji ho vic k ciste funkcionalnim jazykum.
Raději bych použil addEventListener.
Opraveno jest.
Na tyto věci je tu knihovna underscore.js.
Nebo lodash…
Nebo Transducers-js, které můžete použít rovnou s jakýmkoliv Array-like kolekcí případně s kolekcí implementujícím ES6
@@iterator
. A výhodou je mnohem menší paměťová náročnost a mnohem rychlejší zpracování.Stejné benefity získáte i immutabilních kolekcí, které posktuje např. knihovna Immutable.js.
A když se naučíte používat transducery nad poli, můžete pak rovnou začít pracovat i s event streamy, například pomocí knihovny Kefir.js, která má podpru pro transducery vestavěnou.
K čemu je to dobré? Oproti underscore/lodash to vypadá o 2 levely složitější.
2 b. můžeš to tudíž používat na lazy kolekcích (z „pole“ všech přirozených čísel uděláš „pole“ všech prvočísel) aniž by se materializovaly
Dobry clanok ale privital by som keby bolo zmienene of ktorejze verzie JS su ktore funkcie podporovane a tiez ci su nejake nekompatibility v browseroch (vratane mobile). Popripade odkaz na shim…
Dakujem.