Så här använder du Karta, Filter, och Reducera i JavaScript

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.

En karta från lista till lista

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:

  1. De nuvarande föremål i matrisen
  2. De array index av det aktuella objektet
  3. De hela arrayen du ringde på kartan 

Låt oss titta på någon kod.

Karta i praktiken

Anta 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:

  1. Använder sig av Karta, du behöver inte hantera tillståndet för för loop själv.
  2. Du kan använda elementet direkt, snarare än att behöva indexera i arrayen.
  3. Du behöver inte skapa en ny array och 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.
  4. Du do måste komma ihåg att inkludera a 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:

  1. 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.
  2. 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.

gotchas

Å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!

Genomförande

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.

Filtrera ut bruset

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:

  1. De nuvarande föremål 
  2. De nuvarande index
  3. De array du ringde filtrera

filtrera i praktiken

Lå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:

  • Undvik att muta en matris inuti a för varje eller för slinga
  • Tilldela resultatet direkt till en ny variabel, istället för att trycka in i en array som vi definierade någon annanstans

gotchas

Å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.

Genomförande

Å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; ;

Minskar arrays

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 filtreraminska 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:

  1. De nuvarande värde
  2. De föregående värde
  3. De nuvarande index
  4. De array du ringde minska

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 praktiken

Eftersom 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 varjeminska 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.

gotchas

De tre stora gotkorna med minska är:

  1. Glömmer att lämna tillbaka
  2. Glömmer ett initialvärde
  3. Förväntar en array när minska returnerar ett enda värde

Lyckligtvis ä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. 

Genomförande

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:

  1. Den här gången använde jag namnet ackumulator istället för tidigare. Det här är vad du vanligtvis ser i det vilda.
  2. Jag tilldelar 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å.

Sätta det tillsammans: Karta, Filter, Reducera och Chainability

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:

  1. Samla in två dagar av uppgifter.
  2. Konvertera varaktighetstiden till timmar, i stället för minuter.
  3. Filtrera allt som tog två timmar eller mer.
  4. Summa upp det hela.
  5. Multiplicera resultatet med en timmesats för fakturering.
  6. Skriv ut en formaterad dollarbelopp.

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:

  1. Plustecknen framfö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".
  2. Om inte sätta ihop summan inom parentes, 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.

Slutsats & Nästa steg

I den här handledningen har du lärt dig hur Kartafiltrera, 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:

  1. Jafar Husains fantastiska uppsättning övningar om funktionell programmering i JavaScript, komplett med en solid introduktion till Rx.js
  2. Envato Tuts + Instruktör Jason Rhodos kurs om funktionell programmering i JavaScript
  3. Den mest adekvata tillvägagångssättet till funktionell programmering, som går in i större djup på varför vi undviker mutation och funktionellt tänkande i allmänhet

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.