Funktionell programmering har gjort en ganska splash i utvecklingsvärlden dessa dagar. Och med goda skäl: Funktionella tekniker kan hjälpa dig att skriva mer deklarativ kod som är lättare att förstå en överblick, refaktor och test.
En av hörnstenarna i funktionell programmering är dess speciella användning av listor och listoperationer. Och de sakerna är exakt vad ljudet som de är: arrays av saker och saker du gör med dem. Men den funktionella tankegången behandlar dem lite annorlunda än vad man kan förvänta sig.
I den här artikeln kommer jag att titta på vad jag gillar att kalla "stora tre" listoperationerna: Karta
, filtrera
, och minska
. Förpackning av huvudet kring dessa tre funktioner är ett viktigt steg mot att kunna skriva ren funktionskod och öppnar dörrarna till de mycket kraftfulla teknikerna för funktionell och reaktiv programmering.
Det betyder också att du aldrig behöver skriva en för
loop igen.
Nyfiken? Låt oss dyka in.
Ofta finner vi oss själva att behöva ta en matris och modifiera varje element i det på exakt samma sätt. Typiska exempel på detta är att kvadrera varje element i en uppsättning tal, hämta namnet från en lista med användare eller köra en regex mot en rad strängar.
Karta
är en metod byggd för att göra exakt det. Det definieras på Array.prototype
, så du kan ringa den på vilken matris som helst, och det accepterar en återuppringning som sitt första argument.
När du ringer Karta
På en matris, exekverar den återkallelsen på varje element i den, och returnerar a ny array med alla värden som återkallningen returnerade.
Under huven, Karta
skickar tre argument till din återuppringning:
Låt oss titta på någon kod.
Karta
i praktikenAnta att vi har en app som underhåller en rad olika uppgifter för dagen. Varje uppgift
är ett objekt, var och en med a namn
och varaktighet
fast egendom:
// Varaktighet är i minuter var uppgifter = ['namn': 'Skriv för Envato Tuts +', 'duration': 120, 'namn': 'Workout', 'duration': 60 : "Utlåt på Duolingo", "Duration": 240];
Låt oss säga att vi vill skapa en ny array med bara namnet på varje uppgift, så vi kan titta på allt vi har gjort idag. Använder en för
loop, vi skulle skriva något så här:
var task_names = []; för (var i = 0, max = tasks.length; i < max; i += 1) task_names.push(tasks[i].name);
JavaScript erbjuder också a för varje
slinga. Det fungerar som a för
slinga, men hanterar allt rodiness att kontrollera vårt slingindex mot arraylängden för oss:
var task_names = []; tasks.forEach (funktion (uppgift) task_names.push (task.name););
Använder sig av Karta
, vi kan skriva:
var task_names = tasks.map (funktion (uppgift, index, array) return task.name;);
Jag inkluderar index
och array
parametrar för att påminna dig om att de är där om du behöver dem. Eftersom jag inte använde dem här, skulle du kunna lämna dem, och koden skulle fungera bra.
Det finns några viktiga skillnader mellan de båda tillvägagångssätten:
Karta
, du behöver inte hantera tillståndet för för
loop själv.tryck
Gillar det. Karta
returnerar den färdiga produkten på en gång, så vi kan helt enkelt tilldela returvärdet till en ny variabel.lämna tillbaka
uttalande i din återuppringning. Om du inte gör det får du en ny matris fylld med odefinierad
. Visar sig, Allt av de funktioner vi ser på idag delar dessa egenskaper.
Det faktum att vi inte behöver manuellt hantera slingans tillstånd gör vår kod enklare och mer underhållbar. Det faktum att vi kan driva direkt på elementet istället för att behöva indexera i array gör sakerna mer läsbara.
Använder en för varje
loop löser båda dessa problem för oss. Men Karta
har fortfarande minst två olika fördelar:
för varje
avkastning odefinierad
, så det är inte kedjat med andra array metoder. Karta
returnerar en array, så du kan kedja det med andra matrismetoder.Karta
avkastningen matris med den färdiga produkten, snarare än att kräva att vi muterar en matris inuti slingan. Att hålla antalet platser där du ändrar tillståndet till ett absolut minimum är en viktig grund för funktionell programmering. Det ger en säkrare och mer begriplig kod.
Nu är det också en bra tid att påpeka att om du är i Node, testa dessa exempel i Firefox-webbläsarkonsolen eller använda Babel eller Traceur, kan du skriva det här mer kortfattat med ES6-pilfunktioner:
var task_names = tasks.map ((task) => task.name);
Pilfunktionerna låter oss lämna lämna tillbaka
nyckelord i en-liners.
Det blir inte mycket mer läsbart än det.
Återkopplingen du skickar till Karta
måste ha en explicit lämna tillbaka
uttalande eller Karta
kommer att spotta ut en serie full av odefinierad
. Det är inte svårt att komma ihåg att inkludera a lämna tillbaka
värde, men det är inte svårt att glömma.
Om du do glömma bort, Karta
kommer inte att klaga. I stället kommer det tyst att lämna tillbaka en matris full av ingenting. Tysta fel som det kan vara överraskande svårt att felsöka.
Lyckligtvis är det här endast gotcha med Karta
. Men det är en vanlig nog fallgrop som jag är skyldig att betona: Se alltid till att din återuppringning innehåller a lämna tillbaka
påstående!
Att läsa implementeringar är en viktig del av förståelsen. Så, låt oss skriva vår egen lätta vikt Karta
för att bättre förstå vad som händer under huven. Om du vill se en implementering av produktionskvalitet, kolla Mozillas polyfil på MDN.
var map = funktion (array, återuppringning) var new_array = []; array.forEach (funktion (element, index, array) new_array.push (callback (element));); returnera new_array; ; var task_names = map (uppgifter, funktion (uppgift) return task.name;);
Denna kod accepterar en array och en återuppringningsfunktion som argument. Det skapar sedan en ny uppsättning; exekverar återkallelsen på varje element på matrisen vi passerade in; pushar resultaten till den nya arrayen; och returnerar den nya matrisen. Om du kör detta i konsolen får du samma resultat som tidigare. Se bara till att du initialiserar uppgifter
innan du testar det!
Medan vi använder en för loop under huven, förpackar den upp i en funktion döljer detaljerna och låter oss arbeta med abstraktionen istället.
Det gör vår kod mer deklarativ, säger den Vad att göra, inte på vilket sätt att göra det. Du kommer att uppskatta hur mycket mer läsbar, underhållbar och, erm, debuggable Det här kan göra din kod.
Nästa av våra array-operationer är filtrera
. Det gör exakt vad det låter som: Det tar en matris och filtrerar bort oönskade element.
Tycka om Karta
, filtrera
definieras på Array.prototype
. Den är tillgänglig på alla grupper, och du skickar den en återuppringning som sitt första argument. filtrera
utför den återkallelsen på varje element i matrisen och spetsar ut a ny array som innehåller endast de element för vilka återkallelsen återvände Sann
.
Gilla också Karta
, filtrera
skickar din återuppringning tre argument:
filtrera
påfiltrera
i praktikenLåt oss återgå till vårt uppgiftsexempel. I stället för att dra ut namnen på varje uppgift, låt oss säga att jag vill få en lista med bara de uppgifter som tog mig två timmar eller mer för att få gjort.
Använder sig av för varje
, vi skulle skriva:
var difficult_tasks = []; tasks.forEach (funktion (uppgift) if (task.duration> = 120) difficult_tasks.push (uppgift););
Med filtrera
:
var difficult_tasks = tasks.filter (funktion (uppgift) return task.duration> = 120;); // Använda ES6 var difficult_tasks = tasks.filter ((task) => task.duration> = 120);
Här har jag gått och lämnat ut index
och array
argument till vår återkallelse, eftersom vi inte använder dem.
Precis som Karta
, filtrera
låt oss:
för varje
eller för
slingaÅterkopplingen du skickar till Karta
måste inkludera ett returmeddelande om du vill att den ska fungera korrekt. Med filtrera
, du måste också inkludera ett returmeddelande, och du måste se till att det returnerar ett booleskt värde.
Om du glömmer ditt returmeddelande kommer din återuppringning att återvända odefinierad
, som filtrera
kommer ohjälpligt tvinga till falsk
. I stället för att kasta ett fel kommer det tyst att återvända en tom matris!
Om du går den andra vägen, och returnera något som är är inte uttryckligen Sann
eller falsk
, sedan filtrera
kommer att försöka lista ut vad du menade med att tillämpa JavaScript-tvångsregler. Ofta är det inte ett fel. Och precis som att glömma ditt returmeddelande blir det ett tyst.
Alltid Se till att dina återkallelser innehåller ett uttryckligt retureringsformulär. Och alltid se till att dina återuppringningar är in filtrera
lämna tillbaka Sann
eller falsk
. Din sanity kommer att tacka dig.
Återigen är det bästa sättet att förstå ett stycke kod ... ja, att skriva det. Låt oss rulla vår egen lätta vikt filtrera
. De bra på Mozilla har en polyfil för industriell styrka för att du ska läsa också.
var filter = funktion (array, återuppringning) var filtered_array = []; array.forEach (funktion (element, index, array) om (återuppringning (element, index, array)) filtered_array.push (element);); returnera filtered_array; ;
Karta
skapar en ny uppsättning genom att omvandla varje element i en grupp, individuellt. filtrera
skapar en ny uppsättning genom att ta bort element som inte hör hemma. minska
, å andra sidan tar alla element i en array, och minskar dem till ett enda värde.
Precis som Karta
och filtrera
, minska
definieras på Array.prototype
och så tillgänglig på alla grupper, och du skickar en återuppringning som sitt första argument. Men det tar också ett valfritt andra argument: värdet för att börja kombinera alla dina matriselement i.
minska
skickar din återuppringning fyra argument:
minska
påObservera att återuppringningen får en föregående värde vid varje iteration. På den första iterationen, där är inget tidigare värde. Det är därför du har möjlighet att skicka minska
ett initialvärde: Det fungerar som "tidigare värde" för den första iterationen, när det annars inte skulle vara en.
Slutligen, kom ihåg att minska
returnerar a enda värde, inte en array som innehåller ett enda objekt. Detta är viktigare än det kan tyckas, och jag kommer tillbaka till det i exemplen.
minska
i praktikenEftersom minska
är den funktion som människor hittar mest utlännande först, vi börjar med att gå steg för steg genom något enkelt.
Låt oss säga att vi vill hitta summan av en lista med siffror. Med en slinga ser det ut så här:
var tal = [1, 2, 3, 4, 5], totalt = 0; numbers.forEach (funktion (nummer) totalt + = nummer;);
Även om detta inte är ett dåligt användningsfall för för varje
, minska
har fortfarande fördelen att vi tillåter att undvika mutation. Med minska
, vi skulle skriva:
var totalt = [1, 2, 3, 4, 5] .reduce (funktion (föregående, nuvarande) return tidigare + ström;, 0);
Först kallar vi minska
på vår lista över tal.
Vi skickar det en återuppringning, som accepterar föregående värde och nuvärde som argument och returnerar resultatet av att lägga till dem tillsammans. Sedan vi passerade 0
som ett andra argument till minska
, det kommer att använda det som värdet av tidigare
vid den första iterationen.
Om vi tar det steg för steg ser det ut så här:
Iteration | Tidigare | Nuvarande | Total |
---|---|---|---|
1 | 0 | 1 | 1 |
2 | 1 | 2 | 3 |
3 | 3 | 3 | 6 |
4 | 6 | 4 | 10 |
5 | 10 | 5 | 15 |
Om du inte är ett fan av tabeller, kör detta kod i konsolen:
var totalt = [1, 2, 3, 4, 5] .reduce (funktion (tidigare, nuvarande, index) varval = föregående + nuvarande; console.log ("föregående värde är" + tidigare + " värdet är "+ nuvarande +" och den aktuella iterationen är "+ (index + 1)); return val;, 0); console.log ("Slingan är klar och det slutliga värdet är" + totalt + ".");
Att återskapa: minska
iterates över alla element i en array, kombinera dem men du anger i din återuppringning. Vid varje iteration har din återuppringning tillgång till tidigare värde, vilken är total-så-långt, eller ackumulerat värde; de nuvarande värde; de nuvarande index; och hela array, om du behöver dem.
Låt oss återvända till exemplet på våra uppgifter. Vi har fått en lista över uppgiftsnamn från Karta
, och en filtrerad lista över uppgifter som tog lång tid med ... ja, filtrera
.
Vad händer om vi ville veta hur mycket tid vi spenderade idag?
Använder en för varje
loop, skulle du skriva:
var total_time = 0; tasks.forEach (funktion (uppgift) // Plustecknet samlar bara // task.duration från en sträng till ett tal total_time + = (+ task.duration););
Med minska
, det blir:
var total_time = tasks.reduce (funktion (tidigare, nuvarande) returnera tidigare + nuvarande;, 0); // Använda pilfunktioner var total_time = tasks.reduce ((tidigare, nuvarande) tidigare + aktuella);
Lätt.
Det är nästan allt som finns där. Nästan, eftersom JavaScript ger oss en enda mindre känd metod, kallad reduceRight
. I exemplen ovan, minska
började på först objekt i matrisen, iterering från vänster till höger:
var array_of_arrays = [[1, 2], [3, 4], [5, 6]]; var concatenated = array_of_arrays.reduce (funktion (tidigare, nuvarande) return previous.concat (nuvarande);); console.log (konkatenerade); // [1, 2, 3, 4, 5, 6];
reduceRight
gör detsamma, men i motsatt riktning:
var array_of_arrays = [[1, 2], [3, 4], [5, 6]]; var concatenated = array_of_arrays.reduceRight (funktion (tidigare, nuvarande) return previous.concat (nuvarande);); console.log (konkatenerade); // [5, 6, 3, 4, 1, 2];
jag använder minska
varje dag, men jag har aldrig behövt det reduceRight
. Jag tror du förmodligen inte heller. Men om du någonsin gör det, nu vet du att det är där.
De tre stora gotkorna med minska
är:
lämna tillbaka
minska
returnerar ett enda värdeLyckligtvis är de första två lätt att undvika. Att bestämma vad ditt ursprungliga värde ska vara beror på vad du gör, men du kommer snabbt hänga på det.
Den sista kan verka lite underlig. Om minska
returnerar bara någonsin ett enda värde, varför skulle du förvänta dig en array?
Det finns några bra skäl till det. Först, minska
returnerar alltid en enda värde, inte alltid en enda siffra. Om du t.ex. reducerar en rad arrayer kommer den att returnera en enda array. Om du är vana eller reducerar arrays, skulle det vara rättvist att förvänta dig att en array som innehåller ett enda objekt inte skulle vara ett speciellt fall.
För det andra, om minska
gjorde returnera en array med ett enda värde, det skulle naturligtvis fungera bra med Karta
och filtrera
, och andra funktioner på arrays som du sannolikt kommer att använda med den.
Tid för vår sista look under huven. Som vanligt har Mozilla en kollisionsskyddande polyfill för att minska om du vill kolla in det.
var minska = funktion (array, återuppringning, initial) var accumulator = initial || 0; array.forEach (funktion (element) accumulator = callback (ackumulator, array [i]);); retur ackumulator; ;
Två saker att notera här:
ackumulator
istället för tidigare
. Det här är vad du vanligtvis ser i det vilda.ackumulator
ett initialvärde, om en användare tillhandahåller en och standard till 0
, om inte. Så här är det riktiga minska
beter sig också.Vid den här tiden kanske du inte är den där imponerad.
Rimligt nog: Karta
, filtrera
, och minska
, på egen hand, är inte väldigt intressant.
När allt kommer omkring ligger deras sanna makt i deras kedjbarhet.
Låt oss säga att jag vill göra följande:
Låt oss först definiera våra uppgifter för måndag och tisdag:
var måndag = ['namn': 'Skriv en handledning', 'varaktighet': 180, 'namn': 'Vissa webbutvecklingar', 'varaktighet': 120]; var tisdag = ['namn': 'Fortsätt skriva den handledningen', 'duration': 240, 'name': 'Några mer webbutveckling', 'duration': 180, 'name': 'En helhet mycket ingenting "," varaktighet ": 240]; var tasks = [måndag, tisdag];
Och nu, vår snyggingstransformation:
var result = tasks.reduce (funktion (ackumulator, ström) return accumulator.concat (ström);). karta (funktion (uppgift) return (task.duration / 60);). return duration = 2;). karta (funktion (duration) returvaraktighet * 25;). minska (funktion (ackumulator, ström) return [(+ ackumulator) + (+ ström)];). karta (funktion (dollar_amount) return '$' + dollar_amount.toFixed (2);). minska (funktion (formaterad_dollar_amount) return formatted_dollar_amount;);
Eller mer kortfattat:
// Sammanfoga vår 2D-array till en enda lista var result = tasks.reduce ((acc, nuvarande) => acc.concat (aktuellt)) // Ta bort uppgiftslängden och konvertera minuter till timmar. > uppgift.duration / 60) // Filtrera varje uppgift som tog mindre än två timmar .filter ((duration) => duration> = 2) // Multiplicera varje arbetsperiods varaktighet med vår timpris. > Varaktighet * 25) // Kombinera summan till ett enda dollarbelopp .reduce ((acc, nuvarande) => [(+ acc) + (+ ström)]) // Konvertera till en "ganska tryckt" dollarbelopp. karta ((mängd) => '$' + mängd.tillFixed (2)) // Dra ut det enda elementet i matrisen vi kom från kartan .reduce ((formatted_amount) => formatted_amount);
Om du har gjort det här långt, borde det vara ganska enkelt. Det finns dock två bitar av konstighet att förklara.
Först, på rad 10 måste jag skriva:
// Återstående utelämnad minska (funktion (ackumulator, ström) return [(+ ackumulator) + (+ current_];)
Två saker att förklara här:
ackumulator
och nuvarande
tvinga sina värden till nummer. Om du inte gör det kommer returvärdet att vara den ganska värdelösa strängen, "12510075100"
.minska
kommer att spotta ut ett enda värde, inte en array. Det skulle sluta kasta en Skrivfel
, eftersom du bara kan använda Karta
på en array! Den andra biten som kan göra dig lite obekväm är den sista minska
, nämligen:
// Återstående utelämnad karta (funktion (dollar_amount) return '$' + dollar_amount.toFixed (2);). Minska (funktion (formaterad_dollar_amount) return formatted_dollar_amount;);
Det kallar till Karta
returnerar en array som innehåller ett enda värde. Här ringer vi minska
att dra ut det värdet.
Det andra sättet att göra detta skulle vara att ta bort samtalet för att minska, och indexera i arrayen som Karta
spetsar ut:
var result = tasks.reduce (funktion (ackumulator, ström) return accumulator.concat (ström);). karta (funktion (uppgift) return (task.duration / 60);). return duration = 2;). karta (funktion (duration) returvaraktighet * 25;). minska (funktion (ackumulator, ström) return [(+ ackumulator) + (+ ström)];). karta (funktion (dollar_amount) return '$' + dollar_amount.toFixed (2);) [0];
Det är helt korrekt. Om du är mer bekväm med att använda ett arrayindex går du direkt.
Men jag uppmuntrar dig att inte. Ett av de mest kraftfulla sätten att använda dessa funktioner ligger inom reaktiv programmering, där du inte kommer att vara fri att använda arrayindex. Att sparka den vana nu kommer att göra lärande reaktiva tekniker mycket enklare längs linjen.
Slutligen, låt oss se hur vår vän för varje
loop skulle få det gjort:
var concatenated = måndag.concat (tisdag), avgifter = [], formatted_sum, hourly_rate = 25, total_fee = 0; concatenated.forEach (funktion (uppgift) var duration = task.duration / 60; om (duration> = 2) fees.push (duration * hourly_rate);); avgifter. FörEach (funktion (avgift) total_fee + = avgift); var formatted_sum = '$' + total_fee.toFixed (2);
Tolerable, men bullriga.
I den här handledningen har du lärt dig hur Karta
, filtrera
, och minska
arbete; hur man använder dem och ungefär hur de implementeras. Du har sett att de alla tillåter dig att undvika att mutera staten, som använder för
och för varje
öglor kräver, och du borde nu ha en bra uppfattning om hur man kedjar dem alla tillsammans.
Nu är jag säker på att du är ivrig efter att träna och läsa vidare. Här är mina tre bästa förslag på var du ska gå nästa:
JavaScript har blivit ett av de faktiska språken för att arbeta på webben. Det är inte utan sina inlärningskurvor, och det finns gott om ramar och bibliotek för att hålla dig upptagen också. Om du letar efter ytterligare resurser att studera eller använda i ditt arbete, kolla vad vi har tillgängligt på Envato-marknaden.
Om du vill ha mer på den här typen av saker, kolla min profil från tid till annan; fånga mig på Twitter (@PelekeS); eller slå min blogg på http://peleke.me.
Frågor, kommentarer eller förvirring? Lämna dem under, och jag gör mitt bästa för att komma tillbaka till var och en individuellt.
Lär dig JavaScript: Den fullständiga guiden
Vi har byggt en komplett guide för att hjälpa dig att lära dig JavaScript, oavsett om du precis börjat som webbutvecklare eller vill utforska mer avancerade ämnen.