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.
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:
La oss se på noen kode.
kart
i praksisAnta 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:
kart
, du trenger ikke å administrere tilstanden til til
sløyfe deg selv.trykk
i det. kart
returnerer ferdigproduktet på en gang, så vi kan ganske enkelt tildele returverdi til en ny variabel.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:
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.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.
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!
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.
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:
filter
påfilter
i praksisLa 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:
for hver
eller til
sløyfeTilbakekallingen 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 må 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.
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; ;
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 filter
, redusere
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:
redusere
på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 praksisSiden 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 hver
, redusere
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.
De tre store gotkene med redusere
er:
komme tilbake
redusere
returnerer en enkelt verdiHeldigvis 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.
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:
akkumulator
i stedet for tidligere
. Dette er det du vanligvis vil se i naturen.akkumulator
en startverdi, hvis en bruker gir en og standard til 0
, Hvis ikke. Slik er den virkelige redusere
oppfører seg også.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:
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:
akkumulator
og nåværende
tvinge sine verdier til tall. Hvis du ikke gjør dette, blir returverdien den ganske ubrukelige strengen, "12510075100"
.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.
I denne opplæringen har du lært hvordan kart
, filter
, 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:
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.