Slik bruker du Kart, Filter, og Reduser i JavaScript

Funksjonell programmering har gjort ganske skvett i utviklingsverdenen disse dager. Og med god grunn: Funksjonelle teknikker kan hjelpe deg med å skrive mer deklarativ kode som er lettere å forstå på et øyeblikk, refactor og test. 

En av hjørnesteinene til funksjonell programmering er dets spesielle bruk av lister og listeoperasjoner. Og disse tingene er akkurat hva lyden som de er: arrays av ting, og ting du gjør med dem. Men den funksjonelle tankegangen behandler dem litt annerledes enn du kanskje forventer.

Denne artikkelen tar en nærmere titt på hva jeg liker å kalle "store tre" -listen operasjoner: kart, filter, og redusere. Innpakning av hodet rundt disse tre funksjonene er et viktig skritt mot å kunne skrive ren funksjonell kode, og åpner dørene for de svært kraftige teknikkene for funksjonell og reaktiv programmering.

Det betyr også at du aldri trenger å skrive en til sløyfe igjen.

Nysgjerrig? La oss dykke inn.

Et kart fra liste til liste

Ofte finner vi oss selv nødt til å ta en matrise og endre hvert element i den på nøyaktig samme måte. Typiske eksempler på dette er å kvadrere hvert element i en rekke tall, hente navnet fra en liste over brukere, eller kjøre en regex mot en rekke strenger.

kart er en metode bygget for å gjøre akkurat det. Det er definert på Array.prototype, slik at du kan ringe det på en hvilken som helst matrise, og det aksepterer et tilbakering som sitt første argument. 

Når du ringer kart På en matrise utfører den tilbakekallingen på hvert element i den, og returnerer a ny array med alle verdiene som tilbakeringingen returnerte.

Under panseret, kart sender tre argumenter til tilbakeringingen din:

  1. De nåværende element i matrisen
  2. De arrayindeks av gjeldende element
  3. De hele gruppen du ringte kartet på 

La oss se på noen kode.

kart i praksis

Anta at vi har en app som opprettholder en rekke oppgaver for dagen. Hver oppgave er et objekt, hver med a Navn og varighet eiendom:

// Varighet er i minutter var oppgaver = ['navn': 'Skriv for Envato Tuts +', 'duration': 120, 'navn': 'Trening', 'varighet': 60, 'navn' : 'Fremheve Duolingo', 'Varighet': 240];

La oss si at vi vil lage et nytt utvalg med bare navnet på hver oppgave, så vi kan se på alt vi har gjort i dag. Bruker en til loop, vi ville skrive noe slikt:

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

JavaScript tilbyr også a for hver sløyfe. Det fungerer som en til sløyfe, men styrer alt rotet av å sjekke vår løkkeindeks mot matlengden for oss:

var task_names = []; tasks.forEach (funksjon (oppgave) task_names.push (task.name););

Ved hjelp av kart, vi kan skrive:

var task_names = tasks.map (funksjon (oppgave, indeks, array) return task.name;);

Jeg inkluderer index og  matrise parametere for å minne deg om at de er der hvis du trenger dem. Siden jeg ikke brukte dem her, skjønt, kan du legge dem ut, og koden ville gå bra.

Det er noen viktige forskjeller mellom de to tilnærmingene:

  1. Ved hjelp av kart, du trenger ikke å administrere tilstanden til til sløyfe deg selv.
  2. Du kan operere på elementet direkte, heller enn å måtte indeksere i matrisen.
  3. Du trenger ikke å opprette en ny serie og trykk i det. kart returnerer ferdigproduktet på en gang, så vi kan ganske enkelt tildele returverdi til en ny variabel.
  4. Du gjøre må huske å inkludere a komme tilbake uttalelse i tilbakeringingen din. Hvis du ikke gjør det, får du et nytt utvalg fylt med udefinert

Viser seg, alle av funksjonene vi ser på i dag, del disse egenskapene.

Det faktum at vi ikke behøver å håndtere tilstanden til løkken manuelt, gjør koden enklere og mer vedlikeholdsbar. Det faktum at vi kan operere direkte på elementet i stedet for å måtte indeksere i arrayet, gjør ting mer lesbare. 

Bruker en for hver loop løser begge disse problemene for oss. Men kart har fortsatt minst to forskjellige fordeler:

  1. for hver avkastning udefinert, så det er ikke kjedet med andre array metoder. kart returnerer en matrise, så du kan kjede den med andre array metoder.
  2. kart avkastningen matrise med det ferdige produktet, i stedet for å kreve at vi muterer en matrise inne i løkken. 

Å holde antall steder der du endrer tilstand til absolutt minimum, er et viktig prinsipp for funksjonell programmering. Det gir tryggere og mer forståelig kode.

Nå er det også en god tid å påpeke at hvis du er i Node, kan du prøve disse eksemplene i Firefox-nettleserkonsollen, eller ved hjelp av Babel eller Traceur, du kan skrive dette mer konsistent med ES6-pilfunksjoner:

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

Pilfunksjoner lar oss forlate komme tilbake søkeord i en-liners. 

Det blir ikke mye mer lesbart enn det.

gotchas

Tilbakekallingen du sender til kart må ha en eksplisitt komme tilbake uttalelse eller kart vil spytte ut et utvalg fullt av udefinert. Det er ikke vanskelig å huske å inkludere en komme tilbake verdi, men det er ikke vanskelig å glemme. 

Hvis du gjøre glemme, kart vil ikke klage. I stedet vil det stille stille en rekke fulle av ingenting. Stille feil som det kan være overraskende vanskelig å feilsøke. 

Heldigvis er dette den bare kom med kart. Men det er en vanlig nok fallgruve som jeg er forpliktet til å understreke: Sørg alltid for at tilbakeringingen inneholder a komme tilbake uttalelse!

Gjennomføring

Lese implementeringer er en viktig del av forståelsen. Så, la oss skrive vår egen lette vekt kart for bedre å forstå hva som skjer under hetten. Hvis du vil se en implementering av kvalitetskvalitet, sjekk ut Mozillas polyfil på MDN.

var map = funksjon (array, tilbakeringing) var new_array = []; array.forEach (funksjon (element, indeks, array) new_array.push (tilbakeringing (element));); returner new_array; ; var task_names = map (oppgaver, funksjon (oppgave) return task.name;);

Denne koden aksepterer en array og en tilbakeringingsfunksjon som argumenter. Det skaper så et nytt utvalg; utfører tilbakekallingen på hvert element på gruppen vi passerte inn; skyver resultatene inn i det nye systemet; og returnerer den nye arrayen. Hvis du kjører dette i konsollen, får du det samme resultatet som før. Bare vær sikker på at du initialiserer oppgaver før du tester det ut!

Mens vi bruker en for-løkke under hetten, forpakker den opp i en funksjon, skjuler detaljene og lar oss arbeide med abstraksjonen i stedet. 

Det gjør vår kode mer deklarativ, sier det hva å gjøre, ikke hvordan å gjøre det. Du vil sette pris på hvor mye mer lesbar, vedlikeholdbar og, erm, debuggable Dette kan gjøre koden din.

Filter ut støy

Det neste av vår array operasjon er filter. Det gjør akkurat hva det høres ut som: Det tar en matrise, og filtrerer ut uønskede elementer.

Som kart, filter er definert på Array.prototype. Den er tilgjengelig på en hvilken som helst matrise, og du sender den en tilbakering som sitt første argument. filter kjører det tilbakekallingen på hvert element i gruppen, og spytter ut en ny array inneholder bare elementene som tilbakekallingen returnerte til ekte.

Også som kart, filter sender tilbakekallingen tre argumenter:

  1. De nåværende element 
  2. De nåværende indeks
  3. De matrise du ringte filter

filter i praksis

La oss se på vårt oppgaveeksempel. I stedet for å trekke ut navnene på hver oppgave, la oss si at jeg vil få en liste over bare de oppgavene som tok meg to timer eller mer for å få gjort. 

Ved hjelp av for hver, vi ville skrive:

var difficult_tasks = []; tasks.forEach (funksjon (oppgave) if (task.duration> = 120) difficult_tasks.push (oppgave););

Med filter:

var difficult_tasks = tasks.filter (funksjon (oppgave) return task.duration> = 120;); // Bruk av ES6 var difficult_tasks = tasks.filter ((task) => task.duration> = 120);

Her har jeg gått og forlatt index og matrise Argumenter til vårt tilbakeringing, siden vi ikke bruker dem.

Akkurat som kart, filter lar oss:

  • unngå å mutere en matrise inne a for hver eller til sløyfe
  • tilordne resultatet direkte til en ny variabel, i stedet for å trykke inn i en matrise vi definerte andre steder

gotchas

Tilbakekallingen du sender til kart må inkludere en returoppgave hvis du vil at den skal fungere skikkelig. Med filter, du må også inkludere en avkastningserklæring, og deg sørg for at den returnerer en boolsk verdi.

Hvis du glemmer tilbakemeldingen din, vil tilbakeringingen din returnere udefinert, hvilken filter vil unhelpfully tvinge til falsk. I stedet for å kaste en feil, vil den stille en tom rekkefølge stille! 

Hvis du går den andre ruten, og returnere noe som er er ikke eksplisitt ekte eller falsk, deretter filter vil prøve å finne ut hva du mente ved å bruke JavaScript's tvangsregler. Oftere enn ikke, dette er en feil. Og akkurat som å glemme returmeldingen, blir det en stille. 

Alltid sørg for at tilbakeringingene dine inneholder en eksplisitt avkastningserklæring. Og alltid sørg for at tilbakeringingene dine er i filter komme tilbake ekte eller falsk. Din sunnhet vil takke deg.

Gjennomføring

Igjen, den beste måten å forstå et stykke kode på er å skrive det. La oss rulle vår egen lette vekt filter. De gode folkene på Mozilla har en polypropil med industriell styrke, slik at du kan lese også.

var filter = funksjon (array, tilbakeringing) var filtered_array = []; array.forEach (funksjon (element, indeks, array) if (tilbakeringing (element, indeks, array)) filtered_array.push (element);); returner filtered_array; ;

Redusere Arrays

kart lager et nytt utvalg ved å transformere hvert element i en matrise, individuelt. filter lager et nytt utvalg ved å fjerne elementer som ikke tilhører. redusere, På den annen side tar alle elementene i en matrise, og reduserer dem til en enkelt verdi.

Akkurat som kart og filterredusere er definert på Array.prototype og så tilgjengelig på en hvilken som helst matrise, og du sender en tilbakering som sitt første argument. Men det tar også et valgfritt andre argument: verdien for å begynne å kombinere alle elementene dine i. 

redusere sender tilbakekallingen fire argumenter:

  1. De nåværende verdi
  2. De forrige verdi
  3. De nåværende indeks
  4. De matrise du ringte redusere

Legg merke til at tilbakeringingen får en forrige verdi på hver iterasjon. På den første iterasjonen, der er ingen tidligere verdi. Det er derfor du har mulighet til å passere redusere en startverdi: Den fungerer som "tidligere verdi" for den første iterasjonen, da det ellers ikke ville være en.

Endelig husk at redusere returnerer a enkel verdi, ikke en matrise som inneholder et enkelt element. Dette er viktigere enn det kan virke, og jeg kommer tilbake til det i eksemplene.

redusere i praksis

Siden redusere er den funksjonen som folk finner mest fremmede først, begynner vi å gå trinnvis gjennom noe enkelt.

La oss si at vi vil finne summen av en liste over tall. Ved hjelp av en sløyfe ser det slik ut:

var tall = [1, 2, 3, 4, 5], totalt = 0; numbers.forEach (funksjon (tall) totalt + = tall;);

Selv om dette ikke er et dårlig bruk tilfelle for for hverredusere har fortsatt fordelen av å tillate oss å unngå mutasjon. Med redusere, vi ville skrive:

var totalt = [1, 2, 3, 4, 5] .reduce (funksjon (forrige, nåværende) return tidligere + nåværende;, 0);

Først, vi ringer redusere på vår liste over tall. Vi sender det en tilbakeringing, som aksepterer forrige verdi og nåværende verdi som argumenter, og returnerer resultatet av å legge dem sammen. Siden vi passerte 0 som et annet argument til redusere, det vil bruke det som verdien av tidligere på den første iterasjonen.

Hvis vi tar det trinn for trinn, ser det slik ut:

køyring  Tidligere Nåværende Total
1 0 1 1
2 1 2 3
3 3 3 6
4 6 4 10
5 10 5 15

Hvis du ikke er en fan av tabeller, kjør du denne koden i konsollen:

var totalt = [1, 2, 3, 4, 5] .reduce (funksjon (forrige, nåværende, indeks) varval = forrige + nåværende; console.log ("Den forrige verdien er" + forrige + " verdien er "+ nåværende +", og den nåværende iterasjonen er "+ (index + 1)); return val;, 0); console.log ("Sløyfen er ferdig, og den endelige verdien er" + total + ".");

Å omhille: redusere iterates over alle elementene i et array, kombinere dem, men du angir i tilbakeringingen din. Ved hver iterasjon har tilbakekallingen tilgang til tidligere verdi, hvilken er den total-så-langt, eller akkumulert verdi; de nåværende verdi; de nåværende indeks; og hele matrise, hvis du trenger dem.

La oss gå tilbake til vårt oppgavereksempel. Vi har fått en liste over oppgavens navn fra kart, og en filtrert liste over oppgaver som tok lang tid med ... vel, filter

Hva om vi ønsket å vite den totale tiden vi brukte i dag?

Bruker en for hver loop, du vil skrive:

var total_time = 0; tasks.forEach (funksjon (oppgave) // Pluss-tegnet bare coerces // task.duration fra en streng til et tall total_time + = (+ task.duration););

Med redusere, det blir:

var total_time = tasks.reduce (funksjon (tidligere, nåværende) return tidligere + nåværende;, 0); // Bruk av pilfunksjoner var total_time = tasks.reduce ((tidligere, nåværende) forrige + nåværende);

Lett. 

Det er nesten alt der er til det. Nesten fordi JavaScript gir oss en enda mindre kjent metode, kalt reduceRight. I eksemplene ovenfor, redusere startet på først element i arrayet, iterating fra venstre til høyre:

var array_of_arrays = [[1, 2], [3, 4], [5, 6]]; var concatenated = array_of_arrays.reduce (funksjon (forrige, nåværende) return previous.concat (nåværende);); console.log (sammensatt); // [1, 2, 3, 4, 5, 6];

reduceRight gjør det samme, men i motsatt retning:

var array_of_arrays = [[1, 2], [3, 4], [5, 6]]; var concatenated = array_of_arrays.reduceRight (funksjon (tidligere, nåværende) return previous.concat (nåværende);); console.log (sammensatt); // [5, 6, 3, 4, 1, 2];

jeg bruker redusere hver dag, men jeg har aldri trengte det reduceRight. Jeg regner med at du sannsynligvis ikke vil heller. Men i tilfelle du noen gang gjør det, nå vet du at det er der.

gotchas

De tre store gotkene med redusere er:

  1. Glemmer å komme tilbake
  2. Glemmer en startverdi
  3. Forvente en matrise når redusere returnerer en enkelt verdi

Heldigvis er de to første enkle å unngå. Å avgjøre hva din opprinnelige verdi skal være, avhenger av hva du gjør, men du vil få tak i det raskt.

Den siste kan virke litt rar. Hvis redusere bare returnerer en enkelt verdi, hvorfor skulle du forvente en matrise?

Det er noen gode grunner til det. Først, redusere returnerer alltid en enkelt verdi, ikke alltid en enkelt Nummer. Hvis du reduserer en rekke arrays, vil den for eksempel returnere et enkelt array. Hvis du er vant eller reduserer arrays, ville det være rimelig å forvente at en matrise som inneholder et enkelt element ikke ville være et spesielt tilfelle.

For det andre, hvis redusere gjorde returnere en matrise med en enkelt verdi, det ville naturligvis leke fint med kart og filter, og andre funksjoner på arrays som du sannsynligvis vil bruke med den. 

Gjennomføring

Tid for vår siste titt under hetten. Som vanlig har Mozilla en kollisett polyfill for å redusere hvis du vil sjekke det ut.

var redusere = funksjon (array, tilbakeringing, innledende) var accumulator = initial || 0; array.forEach (funksjon (element) akkumulator = tilbakeringing (akkumulator, array [i]);); returnere akkumulator; ;

To ting å merke seg, her:

  1. Denne gangen brukte jeg navnet akkumulator i stedet for tidligere. Dette er det du vanligvis vil se i naturen.
  2. Jeg tildeler akkumulator en startverdi, hvis en bruker gir en og standard til 0, Hvis ikke. Slik er den virkelige redusere oppfører seg også.

Sette det sammen: Kart, Filter, Redusere, og Kjetting

På dette punktet kan du ikke være at imponert. 

Greit nok: kart, filter, og redusere, på egen hånd, er ikke veldig interessant. 

Tross alt ligger deres sanne kraft i deres kjedbarhet. 

La oss si at jeg vil gjøre følgende:

  1. Samle to dager verdt av oppgaver.
  2. Konverter oppgavetidene til timer, i stedet for minutter.
  3. Filter ut alt som tok to timer eller mer.
  4. Samle det hele opp.
  5. Multipliser resultatet med en timepris for fakturering.
  6. Utfør et formatert dollarbeløp.

La oss først definere våre oppgaver for mandag og tirsdag:

var mandag = ['navn': 'Skriv en veiledning', 'varighet': 180, 'navn': 'Noen webutvikling', 'varighet': 120]; var tirsdag = ['navn': 'Fortsett å skrive den opplæringen', 'varighet': 240, 'navn': 'Noen flere webutvikling', 'varighet': 180, 'navn': 'En helhet mye ingenting ',' varighet ': 240]; var tasks = [mandag, tirsdag];

Og nå, vår nydelig utseende transformasjon:

 var result = tasks.reduce (funksjon (akkumulator, nåværende) return accumulator.concat (nåværende);). kart (funksjon (oppgave) return (task.duration / 60);). returvarighet> = 2;). kart (funksjon (varighet) returvarighet * 25;). redusere (funksjon (akkumulator, nåværende) return [(+ akkumulator) + (+ strøm)];). kart (funksjon (dollar_amount) return '$' + dollar_amount.toFixed (2);). redusere (funksjon (formatted_dollar_amount) return formatted_dollar_amount;);

Eller, mer kortfattet:

 // Sammenkoble 2D-arrayet til en enkelt liste var result = tasks.reduce ((acc, nåværende) => acc.concat (nåværende)) // Utdrag oppgavens varighet og konvertere minutter til timer .map ((oppgave) = > oppgave.duration / 60) // Filter ut en oppgave som tok mindre enn to timer. filter ((duration) => duration> = 2) // Multipliser hver oppgaves varighet med timeprisen .map ((duration) = > varighet * 25) // Kombiner summene til en enkelt dollarbeløp .reduce ((acc, nåværende) => [(+ acc) + (+ nåværende)]) // Konverter til et "pen trykt" dollarbeløp. kart ((mengde) => '$' + amount.toFixed (2)) // Trekk ut det eneste elementet i gruppen vi fikk fra kartet .reduce ((formatted_amount) => formatted_amount); 

Hvis du har gjort det så langt, bør dette være ganske greit. Det er to biter av weirdness å forklare, skjønt. 

Først på linje 10 må jeg skrive:

// Resterende utelatt redusere (funksjon (akkumulator, strøm) return [(+ akkumulator) + (+ current_];)

To ting å forklare her:

  1. Pluss tegnene foran akkumulator og nåværende tvinge sine verdier til tall. Hvis du ikke gjør dette, blir returverdien den ganske ubrukelige strengen, "12510075100".
  2. Hvis ikke vikle summen i parentes, redusere vil spytte ut en enkelt verdi, ikke en matrise. Det ville ende med å kaste en Typeerror, fordi du bare kan bruke kart på en matrise! 

Den andre biten som kan gjøre deg litt ubehagelig er den siste redusere, nemlig:

// Resterende utelatt kart (funksjon (dollar_amount) return '$' + dollar_amount.toFixed (2);). Redusere (funksjon (formatted_dollar_amount) return formatted_dollar_amount;);

Det ringer til kart returnerer en matrise som inneholder en enkelt verdi. Her ringer vi redusere å trekke ut den verdien.

Den andre måten å gjøre dette på, er å fjerne anropet for å redusere, og indeksere i arrayet som kart spytter ut:

var result = tasks.reduce (funksjon (akkumulator, nåværende) return accumulator.concat (nåværende);). kart (funksjon (oppgave) return (task.duration / 60);). returvarighet> = 2;). kart (funksjon (varighet) returvarighet * 25;). redusere (funksjon (akkumulator, nåværende) return [(+ akkumulator) + (+ strøm)];). kart (funksjon (dollar_amount) return '$' + dollar_amount.toFixed (2);) [0];

Det er helt riktig. Hvis du er mer komfortabel med å bruke en arrayindeks, gå rett fram.

Men jeg oppfordrer deg til ikke. En av de mest kraftfulle måtene å bruke disse funksjonene er i realm av reaktiv programmering, hvor du ikke vil være fri til å bruke arrayindekser. Sparking den vanen nå vil gjøre læring reaktive teknikker mye lettere nedover linjen.

Til slutt, la oss se hvordan vår venn for hver loop ville få det gjort:

var concatenated = monday.concat (tirsdag), avgifter = [], formatted_sum, hourly_rate = 25, total_fee = 0; concatenated.forEach (funksjon (oppgave) var duration = task.duration / 60; if (duration> = 2) fees.push (duration * hourly_rate);); avgifter. ForEach (funksjon (gebyr) total_fee + = avgift); var formatted_sum = '$' + total_fee.toFixed (2);

Tolerable, men støyende.

Konklusjon og neste trinn

I denne opplæringen har du lært hvordan kartfilter, og redusere arbeid; hvordan å bruke dem og omtrent hvordan de er implementert. Du har sett at de alle tillater deg å unngå muterende tilstand, som bruker til og for hver sløyfer krever, og du bør nå ha en god ide om hvordan å kjede dem alle sammen. 

Nå er jeg sikker på at du er ivrig etter å trene og lese videre. Her er mine tre beste forslag til hvor du skal henge neste:

  1. Jafar Husains fantastiske sett med øvelser om funksjonell programmering i JavaScript, komplett med en solid introduksjon til Rx.js
  2. Envato Tuts + Instruktør Jason Rhodes 'kurs om funksjonell programmering i JavaScript
  3. Den mest hensiktsmessige veiledningen til funksjonell programmering, som går inn i større dybde på hvorfor vi unngår mutasjon og funksjonell tenking generelt

JavaScript har blitt et av de de facto-språkene for å jobbe på nettet. Det er ikke uten sine lærekurver, og det er nok av rammer og biblioteker for å holde deg opptatt også. Hvis du leter etter flere ressurser for å studere eller bruke i arbeidet ditt, sjekk ut hva vi har tilgjengelig på Envato-markedet.

Hvis du vil ha mer på denne typen ting, sjekk profilen min fra tid til annen; få meg på Twitter (@PelekeS); eller slå på bloggen min på http://peleke.me.

Spørsmål, kommentarer eller forvirringer? La dem være under, og jeg vil gjøre mitt beste for å komme tilbake til hver enkelt individuelt.

Lær JavaScript: Den komplette veiledningen

Vi har bygget en komplett guide for å hjelpe deg med å lære JavaScript, enten du er bare i gang som webutvikler eller du vil utforske mer avanserte emner.