Cum se utilizează hartă, filtrare și reducere în JavaScript

Programarea funcțională a făcut destul de mult în lumea dezvoltării în aceste zile. Și pentru un motiv bun: Tehnicile funcționale vă pot ajuta să scrieți un cod declarativ mai clar, ușor de înțeles dintr-o privire, refăctor și test. 

Una dintre pietrele de temelie ale programării funcționale este utilizarea specială a listelor și operațiunilor de listă. Și acele lucruri sunt exact ceea ce sunetul este ca și cum ar fi: o serie de lucruri și lucrurile pe care le faceți pentru ele. Dar mentalitatea funcțională le tratează un pic diferit decât v-ați aștepta.

Acest articol va arunca o privire atentă la ceea ce îmi place să numesc operațiunile din lista celor trei mari: Hartă, filtru, și reduce. Înfășurarea capului în jurul acestor trei funcții reprezintă un pas important spre a putea scrie un cod funcțional curat și deschide ușile spre tehnicile extrem de puternice de programare funcțională și reactivă.

De asemenea, înseamnă că nu va trebui să scrieți niciodată pentru buclă din nou.

Curios? Hai să ne aruncăm.

O hartă de la listă la listă

Adesea, ne aflăm nevoiți să luăm o matrice și să modificăm fiecare element în el exact în același mod. Exemple tipice de acest lucru sunt împărțirea fiecărui element într-o serie de numere, recuperarea numelui dintr-o listă de utilizatori sau rularea unui regex împotriva unui șir de șiruri de caractere.

Hartă este o metodă construită pentru a face exact acest lucru. Este definit pe Array.prototype, astfel încât să o puteți numi pe orice matrice și acceptă un apel invers ca prim argument. 

Când sunați Hartă pe o matrice, execută apelul pe fiecare element din el, returnând a nou array cu toate valorile pe care callback-ul a revenit.

Sub capotă, Hartă transmite trei argumente la apelul dvs. de apel:

  1. curente în matrice
  2. indexul array din elementul curent
  3. întreaga matrice ai sunat harta 

Să ne uităm la un cod.

Hartă in practica

Să presupunem că avem o aplicație care păstrează o serie de sarcini pentru ziua respectivă. Fiecare sarcină este un obiect, fiecare cu un Nume și durată proprietate:

// Duratele sunt în câteva minute var tasks = ['name': 'Scrie pentru Envato Tuts +', 'duration': 120, 'name': ' : "Procrastinate pe Duolingo", "durata": 240];

Să presupunem că vrem să creăm o nouă matrice cu numele fiecărei sarcini, astfel încât să putem arunca o privire la tot ce am făcut astăzi. Folosind un pentru bucla, vom scrie ceva de genul:

var task_names = []; pentru (var i = 0, max = tasks.length; i < max; i += 1)  task_names.push(tasks[i].name); 

JavaScript oferă, de asemenea, a pentru fiecare buclă. Funcționează ca a pentru buclă, dar gestionează toate dezordinea de a verifica indicele bucla noastră împotriva lungimea matrice pentru noi:

var task_names = []; tasks.forEach (funcție (sarcină) task_names.push (task.name););

Utilizarea Hartă, putem scrie:

var task_names = tasks.map (functie (task, index, array) retur task.name;);

Am inclus index și  mulțime parametri pentru a vă reaminti că sunt acolo dacă aveți nevoie de ele. Din moment ce nu le-am folosit aici, le-ai putea lăsa afară, iar codul ar fugi bine.

Există câteva diferențe importante între cele două abordări:

  1. Utilizarea Hartă, nu trebuie să gestionați starea pentru bucurați-vă singur.
  2. Puteți opera direct elementul, mai degrabă decât să trebuiască să indexați în matrice.
  3. Nu trebuie să creați o nouă matrice și Apăsați în ea. Hartă returnează produsul finit într-un singur pas, astfel încât să putem atribui pur și simplu valoarea returnată unei noi variabile.
  4. Tu do trebuie să vă amintiți să includeți a întoarcere în apelul dvs. Dacă nu, veți obține o nouă matrice completă nedefinit

Se pare, toate din funcțiile pe care le vom analiza astăzi împărtășesc aceste caracteristici.

Faptul că nu trebuie să gestionăm manual starea buclei face ca codul nostru să fie mai simplu și mai ușor de întreținut. Faptul că putem opera direct asupra elementului, în loc să trebuiască să indexăm în matrice, face ca lucrurile să fie mai ușor de citit. 

Folosind un pentru fiecare buclă rezolvă ambele probleme pentru noi. Dar Hartă are încă cel puțin două avantaje distincte:

  1. pentru fiecare se intoarce nedefinit, astfel încât nu lanț cu alte metode de matrice. Hartă returnează o matrice, deci tu poate sa lant cu alte metode de matrice.
  2. Hartă se intoarceo matrice cu produsul finit, în loc să ne solicităm să mutați o matrice în bucla. 

Menținerea numărului de locuri în care modificați starea la un minim absolut este un principiu important al programării funcționale. Aceasta face ca un cod mai sigur și mai inteligibil.

Acum este, de asemenea, un moment bun pentru a sublinia că dacă sunteți în Nod, testați aceste exemple în consola browser-ului Firefox sau folosind Babel sau Traceur, puteți scrie mai concis cu funcțiile săgeată ES6:

var task_names = tasks.map ((task) => task.name);

Funcțiile cu săgeți ne lasă să renunțăm întoarcere cuvânt cheie în unul-liners. 

Nu este mult mai ușor de citit decât asta.

Gotchas

Apelul la care treci Hartă trebuie să aibă un explicit întoarcere declarație, sau Hartă va scuipa o gamă plină de nedefinit. Nu este greu să vă amintiți să includeți a întoarcere valoare, dar nu este greu de uitat. 

daca tu do a uita, Hartă nu se va plânge. În schimb, va trimite în liniște o matrice plină de nimic. Silent erori de genul asta pot fi surprinzător de greu de depanat. 

Din fericire, acesta este numai gotcha cu Hartă. Dar este o capcană destul de comună pe care trebuie să o subliniez: Întotdeauna asigurați-vă că apelul dvs. conține un a întoarcere afirmație!

Punerea în aplicare

Implementarea lecturii este o parte importantă a înțelegerii. Deci, să scriem propria noastră ușoară Hartă pentru a înțelege mai bine ce se întâmplă sub capotă. Dacă doriți să vedeți o implementare de calitate a producției, verificați poliflul Mozilla de la MDN.

var hartă = funcție (array, callback) var new_array = []; array.forEach (funcție (element, index, array) new_array.push (callback (element));); retur nou_array; ; var task_names = hartă (sarcini, funcție (sarcină) return task.name;);

Acest cod acceptă ca argumente o matrice și o funcție de apel invers. Apoi creează o nouă matrice; execută apelul invers la fiecare element din matricea în care am trecut; împinge rezultatele în noua matrice; și returnează noua matrice. Dacă executați acest lucru în consola dvs., veți obține același rezultat ca înainte. Asigurați-vă că inițializați sarcini înainte de a le testa!

În timp ce folosim o buclă pentru sub capotă, înfășurarea acesteia într-o funcție ascunde detaliile și ne permite să lucrăm cu abstractizarea. 

Asta face codul nostru mai declarativ - se spune ce să facă, nu Cum să o facă. Veți aprecia cât de mult mai ușor de citit și mai ușor de întreținut, debuggable acest lucru poate face codul dvs..

Filtrați zgomotul

Următoarea operațiune de array este filtru. Ea face exact ceea ce pare: Este nevoie de o matrice și filtrează elemente nedorite.

Ca Hartă, filtru este definită pe Array.prototype. Este disponibil pe orice matrice și îi transmiteți un apel ca primul său argument. filtru execută apelul pe fiecare element al matricei și scutură a nou array conținând numai elementele pentru care apelul a revenit Adevărat.

De asemenea, cum ar fi Hartă, filtru transmite apelul dvs. trei argumente:

  1. curente 
  2. indicele actual
  3.  mulțime tu ai sunat filtru pe

filtru in practica

Să revedem exemplul nostru de activitate. În loc să scoatem numele fiecărei sarcini, să spunem că vreau să obțin o listă a sarcinilor care mi-au luat două ore sau mai multe pentru a obține. 

Utilizarea pentru fiecare, am scrie:

var dificil_tasks = []; tasks.forEach (funcție (sarcină) if (task.duration> = 120) difficult_tasks.push (task););

Cu filtru:

var difficult_tasks = tasks.filter (funcție (sarcină) return task.duration> = 120;); // Utilizarea ES6 var dificil_tasks = tasks.filter ((task) => task.duration> = 120);

Aici, am mers înainte și l-am lăsat afară index și mulțime argumente pentru apelul nostru, deoarece nu le folosim.

Ca și cum Hartă, filtru Ne lasă:

  • evitați mutarea unei matrice în a pentru fiecare sau pentru buclă
  • atribuiți rezultatul direct unei noi variabile, mai degrabă decât să împingeți într-o matrice pe care am definit-o în altă parte

Gotchas

Apelul la care treci Hartă trebuie să includă o declarație de returnare dacă doriți să funcționeze corect. Cu filtru, trebuie să includeți și o declarație de returnare, și tu trebuie sa asigurați-vă că returnează o valoare booleană.

Dacă ați uitat declarația de returnare, apelul dvs. va reveni nedefinit, care filtru va constrânge necorespunzător fals. În loc să arunce o eroare, va întoarce în tăcere o matrice goală! 

Dacă mergeți pe celălalt traseu și returnați ceva care este nu este în mod explicit Adevărat sau fals, atunci filtru va încerca să dau seama ce ați vrut să faceți prin aplicarea regulilor de constrângere ale JavaScript. De cele mai multe ori, acesta este un bug. Și, ca și cum ați uita declarația de returnare, va fi una tăcută. 

Mereu asigurați-vă că apelurile dvs. includ o declarație explicită de returnare. Și mereu asigurați-vă că apelurile dvs. sunt înăuntru filtru întoarcere Adevărat sau fals. Sanctitudinea voastră vă va mulțumi.

Punerea în aplicare

Încă o dată, cea mai bună modalitate de a înțelege o bucată de cod este ... să o scrieți. Să ne răsuciți ușor filtru. Oamenii buni de la Mozilla au un polyfill de putere industrială pentru tine de citit.

var filter = funcție (matrice, apel invers) var filtered_array = []; array.forEach (funcție (element, index, array) if (apel invers (element, index, array)) filtered_array.push (element);); returnează filtered_array; ;

Reducerea matricelor

Hartă creează o nouă matrice transformând fiecare element într-o matrice, în mod individual. filtru creează o nouă matrice eliminând elemente care nu aparțin. reduce, pe de altă parte, ia toate elementele într - o matrice și reduce într-o singură valoare.

Ca și cum Hartă și filtrureduce este definită pe Array.prototype și astfel disponibilă pe orice matrice, iar dvs. primiți un apel invers ca prim argument. Dar, de asemenea, este nevoie de un al doilea argument opțional: valoarea pentru a începe combinarea tuturor elementelor dvs. de matrice în. 

reduce transmite apelul dvs. cu patru argumente:

  1. Valoarea curentă
  2. valoare anterioară
  3. indicele actual
  4. mulțime tu ai sunat reduce pe

Observați că callback-ul primește a valoare anterioară pe fiecare iterație. La prima repetare, acolo este nici o valoare anterioară. De aceea aveți opțiunea de a trece reduce o valoare inițială: acționează ca "valoarea anterioară" pentru prima iterație, atunci când altfel nu ar fi una.

În cele din urmă, rețineți asta reduce returnează a valoare unică, nu o matrice care conține un singur element. Acest lucru este mai important decât s-ar părea și voi reveni la el în exemple.

reduce in practica

De cand reduce este funcția pe care oamenii o găsesc cel mai străin de la început, vom începe prin mersul pas cu pas prin ceva simplu.

Să presupunem că vrem să găsim suma unei liste de numere. Folosind o buclă, care arată astfel:

var numere = [1, 2, 3, 4, 5], total = 0; numbers.forEach (funcție (număr) total + = număr;);

Deși acest lucru nu este un caz de utilizare rău pentru pentru fiecarereduce are încă avantajul de a ne permite să evităm mutația. Cu reduce, am scrie:

var total = [1, 2, 3, 4, 5] .reduceți (funcția (anterioară, actuală) returnați anterior + curent;, 0);

În primul rând, sunăm reduce pe lista noastră de numerele. Îi transmitem un apel invers, care acceptă valoarea anterioară și valoarea curentă ca argumente, și returnează rezultatul adăugării acestora. De când am trecut 0 ca un al doilea argument reduce, se va folosi ca valoare de anterior la prima iterație.

Dacă o luăm pas cu pas, se va arăta astfel:

Repetare  Anterior Actual Total
1 0 1 1
2 1 2 3
3 3 3 6
4 6 4 10
5 10 5 15

Dacă nu sunteți un fan al meselor, rulați acest fragment în consola:

var total = [1, 2, 3, 4, 5] .reduceți (funcția (precedent, curent, index) var val = precedent + curent; valoarea este "+ curent +", iar iterația curentă este "+ (index + 1)); retur val;, 0); console.log ("Bucla este terminată și valoarea finală este" + total + ".);

A recapitula: reduce iterează peste toate elementele unei matrice, combinându-le cu toate acestea pe care le specificați în apelul dvs. de apel. La fiecare repetare, apelul dvs. are acces la anterior valoare, care este total de-așa-departe, sau acumulatăValoarea curentăindicele actual; și întreaga mulțime, dacă aveți nevoie de ele.

Să revenim la exemplul sarcinilor noastre. Am obținut o listă de nume de sarcini Hartă, și o listă filtrată de sarcini care au durat mult timp cu ... bine, filtru

Dacă am vrea să cunoaștem timpul petrecut astăzi?

Folosind un pentru fiecare bucla, ai scrie:

var total_time = 0; tasks.forEach (funcție (sarcină) // Semnul plus doar coerces // task.duration de la un String la un număr total_time + = (+ task.duration););

Cu reduce, care devine:

var total_time = tasks.reduce (funcția (anterioară, curentă) returnați anterior + curent;, 0); // Utilizarea funcțiilor săgeți var total_time = tasks.reduce ((precedent, actual) precedent + curent);

Uşor. 

Asta e aproape totul. Aproape, pentru că JavaScript ne oferă o metodă puțin cunoscută, numită reduceRight. În exemplele de mai sus, reduce a început la primul element din matrice, iterând de la stânga la dreapta:

var array_of_arrays = [[1, 2], [3, 4], [5, 6]]; var concatenated = array_of_arrays.reduce (funcția (precedentă, actuală) returnați anterior.concat (curent);); console.log (concatenate); // [1, 2, 3, 4, 5, 6];

reduceRight face același lucru, dar în direcția opusă:

var array_of_arrays = [[1, 2], [3, 4], [5, 6]]; var concatenated = array_of_arrays.reduceRight (funcția (anterioară, actuală) returnă previous.concat (current);); console.log (concatenate); // [5, 6, 3, 4, 1, 2];

eu folosesc reduce în fiecare zi, dar nu am nevoie niciodată reduceRight. Cred, probabil, că nu o veți face. Dar în cazul în care faceți vreodată, acum știți că este acolo.

Gotchas

Cei trei mari gotchas cu reduce sunteți:

  1. Uitând la întoarcere
  2. Uitarea unei valori inițiale
  3. Se așteaptă o matrice când reduce returnează o singură valoare

Din fericire, primele două sunt ușor de evitat. Deciderea a ceea ce ar trebui să fie valoarea inițială depinde de ceea ce faci, dar vei avea parte de el repede.

Ultimul ar părea un pic ciudat. Dacă reduce doar întoarce vreodată o singură valoare, de ce te-ai aștepta la o matrice?

Există câteva motive bune pentru asta. Primul, reduce întoarce întotdeauna un singur valoare, nu întotdeauna un singur număr. Dacă reduceți o matrice de matrice, de exemplu, va reveni la o singură matrice. Dacă vă aflați în obișnuință sau în reducerea matricelor, ar fi corect să vă așteptați ca o matrice care conține un singur element să nu fie un caz special.

În al doilea rând, dacă reduce făcut întoarce un matrice cu o singură valoare, ar juca frumos frumos cu Hartă și filtru, și alte funcții pe tablourile pe care probabil că le folosiți. 

Punerea în aplicare

Timpul pentru ultimul nostru aspect sub capota. Ca de obicei, Mozilla are un polyfill antiglont pentru reducere, dacă doriți să o verificați.

var reduce = funcția (matrice, apel invers, inițial) var acumulator = inițial || 0; array.forEach (funcție (element) acumulator = apel invers (acumulator, matrice [i]);); acumulator de retur; ;

Două lucruri de reținut, aici:

  1. De data asta am folosit numele acumulator in loc de anterior. Aceasta este ceea ce veți vedea de obicei în sălbăticie.
  2. Eu aloc acumulator o valoare inițială, dacă un utilizator oferă unul, și implicit la 0, dacă nu. Așa este realul reduce se comporta, de asemenea.

Punerea împreună: harta, filtrarea, reducerea și rentabilitatea

În acest moment, s-ar putea să nu fiți acea impresionat. 

Destul de corect: Hartă, filtru, și reduce, pe cont propriu, nu sunt extrem de interesante. 

La urma urmei, adevărata lor putere constă în capacitatea lor de a fi înlănțuite. 

Să spunem că vreau să fac următoarele:

  1. Colectați două zile de sarcini.
  2. Conversia duratelor sarcinii în ore, în loc de minute.
  3. Filtrați tot ceea ce a durat două ore sau mai mult.
  4. Sinteti totul.
  5. Înmulțiți rezultatul cu o rată per-oră pentru facturare.
  6. Efectuați o sumă formatată în dolari.

Mai întâi, să definim sarcinile noastre luni și marți:

var monday = ['nume': 'Scrie un tutorial', 'durata': 180, 'nume': 'Unele dezvoltări web', 'durata': 120]; var tuesday = ['nume': 'Păstrați scris acest tutorial', 'durata': 240, 'nume': ' lot de nimic "," durată ": 240]; var sarcinile = [luni, marți];

Și acum, transformarea noastră minunată:

 var rezultat = tasks.reduce (functie (acumulator, curent) retur accumulator.concat (curent);) harta (function (task) return (task.duration / 60) return (= retur> = 2;) hartă (funcție (durată) durata returului * 25;)) reduce (funcția (acumulator, curent) return + (+ curent); hartă (funcție (dollar_amount) return '$' + dollar_amount.toFixed (2);)) reduce (funcția (formatted_dollar_amount) return formatted_dollar_amount;);

Sau, mai concis:

 // concatenați matricea noastră 2D într-o singură listă var rezultat = tasks.reduce ((acc, current) => acc.concat (current)) // extrage durata sarcinii și convertește minutele la ore .map ((task) = > task.duration / 60) // Filtrați orice activitate care a durat mai puțin de două ore .filter ((duration) => duration> = 2) // Multiplicați durata fiecărei sarcini cu rata orară .map ((duration = > durata = 25) // Combinați sumele într-o singură sumă de dolar. reduce ((acc, current) => [(+ acc) + (+ curent)]) // Convertiți la o sumă dolar "destul de tipărită". map ((sumă) => '$' + amount.toFixed (2)) // Trageți singurul element al matricei de pe hartă .reduce ((formatted_amount) => formatted_amount); 

Dacă ați reușit acest lucru, acest lucru ar trebui să fie destul de simplu. Există însă două fragmente de ciudățenie care trebuie explicate. 

În primul rând, pe linia 10, trebuie să scriu:

// Remainder omit reduce (funcție (acumulator, curent) return [(+ acumulator) + (+ curent_];)

Două lucruri de explicat aici:

  1. Semnele plus în fața lui acumulator și actual constrânge valorile lor la numere. Dacă nu faceți acest lucru, valoarea returnată va fi șirul inutil, "12510075100".
  2. Dacă nu încadrați suma în paranteze, reduce va scuipa o singură valoare, nu o matrice. Asta ar termina aruncarea a Eroare de scris, pentru că puteți utiliza doar Hartă pe o matrice! 

Cel de-al doilea bit care te-ar putea face un pic incomod este ultima reduce, și anume:

// Remainder omit harta (functie (dollar_amount) return '$' + dollar_amount.toFixed (2);)) reduce (functie (formatted_dollar_amount) retur formatted_dollar_amount;);

Apelarea Hartă returnează o matrice care conține o singură valoare. Aici sunăm reduce pentru a scoate această valoare.

Celălalt mod de a face acest lucru ar fi să eliminați apelul de reducere și să indexați în matricea respectivă Hartă scuipă:

var rezultat = tasks.reduce (functie (acumulator, curent) retur accumulator.concat (curent);) harta (function (task) return (task.duration / 60) return (= retur> = 2;) hartă (funcție (durată) durata returului * 25;)) reduce (funcția (acumulator, curent) return + (+ curent); hartă (funcție (dollar_amount) return '$' + dollar_amount.toFixed (2);) [0];

Este perfect corect. Dacă sunteți mai confortabil folosind un index de matrice, mergeți înainte.

Dar vă încurajez să nu faceți asta. Una dintre cele mai puternice căi de a utiliza aceste funcții este în domeniul programării reactive, unde nu veți fi liberi să folosiți indicii matrice. Impunând acest obicei, tehnicile reactive de învățare vor fi mult mai ușoare pe linie.

În cele din urmă, să vedem cum prietenul nostru pentru fiecare buclă ar fi făcut-o:

var concatenated = monday.concat (marți), taxe = [], formatted_sum, hourly_rate = 25, total_fee = 0; concatenated.forEach (funcție (sarcină) var duration = task.duration / 60; dacă (duration> = 2) fees.push (duration * hourly_rate);); taxe pentru fiecare (funcție (comision) total_fee + = taxă); var formatted_sum = '$' + total_fee.toFixed (2);

Tolerabil, dar zgomotos.

Concluzie și pașii următori

În acest tutorial, ați învățat cum Hartăfiltru, și reduce muncă; cum să le folosiți; și modul în care sunt implementate. Ați văzut că toți vă permit să evitați să mutați statul, pe care îl utilizați pentru și pentru fiecare buclele necesită și acum ar trebui să aveți o idee bună despre cum să le lanseze împreună. 

Până acum, sunt sigur că ești dornic să practice și să citești în continuare. Iată primele trei sugestii pentru locația următoare:

  1. Setul superb de exerciții Jafar Husain privind programarea funcțională în JavaScript, complet cu o introducere solidă în Rx.js
  2. Instructorul Envato Tuts + Jason Rhodes pentru programarea funcțională în JavaScript
  3. Ghidul cel mai adecvat pentru programarea funcțională, care merge mai adânc în ceea ce privește motivul pentru care evităm mutația și gândirea funcțională în general

JavaScript a devenit una dintre limbile de-facto de lucru pe web. Nu este fără curbele sale de învățare, și există o mulțime de cadre și biblioteci pentru a vă menține ocupați. Dacă sunteți în căutarea unor resurse suplimentare pentru a studia sau a utiliza în munca dvs., verificați ce avem la dispoziție pe piața Envato.

Dacă doriți mai mult pentru astfel de lucruri, verificați din când în când profilul meu; prinde-mă pe Twitter (@PelekeS); sau mi-a lovit blogul la http://peleke.me.

Întrebări, comentarii sau confuzii? Lăsați-le mai jos și voi face tot posibilul pentru a reveni la fiecare în parte.

Aflați JavaScript: Ghidul complet

Am creat un ghid complet care vă ajută să învățați JavaScript, indiferent dacă începeți doar ca dezvoltator web sau doriți să explorați subiecte mai avansate.

Cod