Map, filter en verkleinen gebruiken in JavaScript

Functioneel programmeren heeft in de ontwikkelingswereld behoorlijk veel indruk gemaakt. En terecht: functionele technieken kunnen u helpen meer declaratieve code te schrijven die gemakkelijker te begrijpen is in een oogopslag, een refactor en een test.. 

Een van de hoekstenen van functioneel programmeren is het speciale gebruik van lijsten en lijstbewerkingen. En die dingen zijn precies hoe het klinkt alsof ze zijn: arrays van dingen en de dingen die je ze aandoet. Maar de functionele mindset behandelt ze een beetje anders dan je zou verwachten.

Dit artikel zal eens kijken naar wat ik graag de "grote drie" lijstbewerkingen noem: kaart, filter, en verminderen. Het omwikkelen van je hoofd rond deze drie functies is een belangrijke stap in de richting van het kunnen schrijven van schone functionele code, en opent de deuren naar de enorm krachtige technieken van functionele en reactieve programmering.

Het betekent ook dat je nooit een script hoeft te schrijven voor loop opnieuw.

Nieuwsgierig? Laten we erin duiken.

Een kaart van lijst naar lijst

Vaak moeten we een array maken en elk element daarin op precies dezelfde manier wijzigen. Typische voorbeelden hiervan zijn het kwadrateren van elk element in een reeks getallen, het ophalen van de naam uit een lijst met gebruikers of het uitvoeren van een regex tegen een reeks tekenreeksen.

kaart is een methode die is gebouwd om precies dat te doen. Het is gedefinieerd op Array.prototype, zodat je het op elke array kunt gebruiken, en het accepteert een callback als zijn eerste argument. 

Wanneer je belt kaart op een array voert het die callback uit op elk element erin, en retourneert een nieuwe array met alle waarden die door de callback zijn geretourneerd.

Onder de motorkap, kaart geeft drie argumenten door aan je callback:

  1. De huidig ​​item in de array
  2. De array-index van het huidige item
  3. De hele array je hebt kaart gebeld 

Laten we naar een code kijken.

kaart in praktijk

Stel dat we een app hebben die een hele reeks taken voor vandaag bijhoudt. Elk taak is een object, elk met een naam en looptijd eigendom:

// Duraties zijn in minuten var tasks = ['name': 'Write for Envato Tuts +', 'duration': 120, 'name': 'Work out', 'duration': 60, 'name' : 'Procrastinate on Duolingo', 'duration': 240];

Laten we zeggen dat we een nieuwe array willen maken met alleen de naam van elke taak, zodat we alles kunnen bekijken wat we vandaag hebben gedaan. Met behulp van een voor loop, zouden we zoiets als dit schrijven:

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

JavaScript biedt ook een forEach lus. Het functioneert als een voor loop, maar beheert alle de rommeligheid van het controleren van onze lus index tegen de lengte van de array voor ons:

var task_names = []; tasks.forEach (functie (taak) task_names.push (task.name););

Gebruik makend van kaart, we kunnen schrijven:

var task_names = taken.map (functie (taak, index, array) return task.name;);

Ik neem de inhoudsopgave en  rangschikking parameters om u eraan te herinneren dat ze er zijn als u ze nodig hebt. Omdat ik ze hier niet gebruikte, kon je ze echter weglaten en zou de code prima werken.

Er zijn een paar belangrijke verschillen tussen de twee benaderingen:

  1. Gebruik makend van kaart, je hoeft niet de staat van de te beheren voor loop jezelf.
  2. U kunt het element rechtstreeks bewerken, in plaats van dat u in de array moet indexeren.
  3. U hoeft geen nieuwe array te maken en Duwen erin. kaart retourneert het voltooide product in één keer, zodat we de retourwaarde eenvoudig aan een nieuwe variabele kunnen toewijzen.
  4. U do moet onthouden om een terugkeer verklaring in uw terugroepactie. Als je dat niet doet, krijg je een nieuwe array gevuld met onbepaald

Blijkt, allemaal van de functies die we vandaag zullen bekijken, delen deze kenmerken.

Het feit dat we de status van de lus niet handmatig hoeven te beheren, maakt onze code eenvoudiger en beter onderhouden. Het feit dat we direct op het element kunnen werken in plaats van dat we in de array moeten indexeren, maakt dingen leesbaarder. 

Met behulp van een forEach loop lost deze beide problemen voor ons op. Maar kaart heeft nog steeds ten minste twee duidelijke voordelen:

  1. forEach komt terug onbepaald, dus het ketenen niet met andere array-methoden. kaart geeft een array terug, dus jij kan keten het met andere array-methoden.
  2. kaart komt terugeen array met het eindproduct, in plaats van dat we een array binnen de lus moeten muteren. 

Het houden van het aantal plaatsen waar u de toestand wijzigt tot een absoluut minimum is een belangrijk principe van functionele programmering. Het zorgt voor een veiligere en begrijpelijkere code.

Het is ook een goed moment om erop te wijzen dat als u in Node bent, deze voorbeelden test in de Firefox-browserconsole of Babel of Traceur gebruikt, u dit beknopter kunt schrijven met ES6-pijlfuncties:

var task_names = taken.map ((taak) => task.name);

Met pijlfuncties kunnen we de terugkeer sleutelwoord in oneliners. 

Het wordt niet veel leesbaarder dan dat.

gotchas

Het terugbellen dat u doorgeeft kaart moet een expliciete hebben terugkeer verklaring, of kaart zal een reeks vol spugen onbepaald. Het is niet moeilijk om te onthouden om een terugkeer waarde, maar het is niet moeilijk om te vergeten. 

als jij do vergeten, kaart zal niet klagen. In plaats daarvan zal het stil een array vol met niets teruggeven. Dergelijke stille fouten kunnen verrassend moeilijk te debuggen zijn. 

Gelukkig is dit het enkel en alleen gotcha met kaart. Maar het is een gebruikelijke valkuil die ik moet benadrukken: zorg er altijd voor dat je callback een bevat terugkeer uitspraak!

Implementatie

Het lezen van implementaties is een belangrijk onderdeel van begrip. Dus laten we ons eigen lichtgewicht schrijven kaart om beter te begrijpen wat er onder de motorkap gebeurt. Als u een productiekwaliteitsimplementatie wilt zien, bekijk dan Mozilla's polyfill op MDN.

var map = function (array, callback) var new_array = []; array.forEach (functie (element, index, array) new_array.push (callback (element));); return new_array; ; var task_names = map (taken, functie (taak) return task.name;);

Deze code accepteert een array en een callback-functie als argumenten. Vervolgens wordt een nieuwe array gemaakt; voert de callback uit op elk element in de array die we hebben doorgegeven; duwt de resultaten naar de nieuwe array; en retourneert de nieuwe array. Als u dit in uw console uitvoert, krijgt u hetzelfde resultaat als voorheen. Zorg ervoor dat u initialiseert taken voordat je het uittest!

Terwijl we een for-lus onder de motorkap gebruiken, verbergt het inpakken van een functie de details en laat ons in plaats daarvan werken met de abstractie. 

Dat maakt onze code meer declaratief, zegt hij wat doen, niet doen hoe om het te doen. U zult waarderen hoeveel leesbaarder, onderhoudbaarder en ermer is, foutopsporing dit kan je code maken.

Filter de ruis uit

De volgende van onze array-bewerkingen is filter. Het doet precies hoe het klinkt: het kost een array en filtert ongewenste elementen weg.

Net zoals kaart, filter is gedefinieerd op Array.prototype. Het is beschikbaar op elke array en u geeft het een callback door als eerste argument. filter voert die callback uit op elk element van de array en spuugt a uit nieuwe array met enkel en alleen de elementen waarvoor de callback is geretourneerd waar.

Ook als kaart, filter geeft je callback drie argumenten door:

  1. De huidig ​​item 
  2. De huidige index
  3. De rangschikking jij hebt gebeld filter op

filter in praktijk

Laten we ons voorbeeld van de taak opnieuw bekijken. In plaats van de namen van elke taak eruit te trekken, laten we zeggen dat ik een lijst wil zien van alleen de taken waarvoor ik twee uur of langer nodig had om klaar te zijn. 

Gebruik makend van forEach, we zouden schrijven:

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

Met filter:

var difficult_tasks = tasks.filter (functie (taak) return task.duration> = 120;); // Gebruik van ES6 var difficult_tasks = tasks.filter ((taak) => task.duration> = 120);

Hier ben ik verder gegaan en de inhoudsopgave en rangschikking argumenten voor ons terugbellen, omdat we ze niet gebruiken.

Net als kaart, filter laat ons:

  • vermijd het muteren van een array binnen een forEach of voor lus
  • wijs het resultaat rechtstreeks toe aan een nieuwe variabele, in plaats van in een array te duwen die we elders hebben gedefinieerd

gotchas

Het terugbellen dat u doorgeeft kaart moet een retourinstructie opnemen als u wilt dat deze correct functioneert. Met filter, je moet ook een retourverklaring en jij vermelden moet zorg ervoor dat het een Booleaanse waarde retourneert.

Als u uw retourverklaring vergeet, keert uw terugbelfunctie terug onbepaald, welke filter zal nutteloos dwingen aan vals. In plaats van een fout te genereren, zal het geruisloos een lege array retourneren! 

Als je de andere route volgt en iets terugstuurt dat dat is is niet expliciet waar of vals, dan filter zal proberen te achterhalen wat je bedoelde met het toepassen van de dwangregels van JavaScript. Vaker wel dan niet, dit is een bug. En net als het vergeten van uw terugkeerverklaring, zal het een stille zijn. 

Altijd zorg ervoor dat uw callbacks een expliciete retourverklaring bevatten. En altijd zorg ervoor dat je terugbelt filter terugkeer waar of vals. Je geestelijke gezondheid zal je bedanken.

Implementatie

Nogmaals, de beste manier om een ​​stukje code te begrijpen is ... nou ja, om het te schrijven. Laten we onze eigen lichtgewicht rollen filter. De goede mensen van Mozilla hebben ook een polyfill op industriële sterkte om te lezen.

var filter = function (array, callback) var filtered_array = []; array.forEach (functie (element, index, array) if (callback (element, index, array)) filtered_array.push (element);); return filtered_array; ;

Het verminderen van arrays

kaart maakt een nieuwe array door elk element in een array afzonderlijk te transformeren. filter maakt een nieuwe array door elementen te verwijderen die er niet bij horen. verminderen, aan de andere kant, neemt alle elementen in een array, en reduceert ze in een enkele waarde.

Net als kaart en filterverminderen is gedefinieerd op Array.prototype en dus beschikbaar op elke array, en u geeft een callback door als eerste argument. Maar er is ook een optioneel tweede argument voor nodig: de waarde om te beginnen met het combineren van al uw array-elementen. 

verminderen geeft je callback vier argumenten door:

  1. De huidige waarde
  2. De vorige waarde
  3. De huidige index
  4. De rangschikking jij hebt gebeld verminderen op

Merk op dat de callback een krijgt vorige waarde op elke iteratie. Over de eerste iteratie, daar is geen vorige waarde. Dit is waarom je de optie hebt om te slagen verminderen een initiële waarde: het fungeert als de "vorige waarde" voor de eerste iteratie, wanneer er anders geen zou zijn.

Houd er tenslotte rekening mee dat verminderen geeft a terug enkele waarde, niet een array met een enkel item. Dit is belangrijker dan het lijkt, en ik kom hierop terug in de voorbeelden.

verminderen in praktijk

Sinds verminderen is de functie die mensen in eerste instantie het meest vreemd vinden, we beginnen met stap voor stap door iets eenvoudigs te lopen.

Laten we zeggen dat we de som van een lijst met getallen willen vinden. Met behulp van een lus ziet dat er zo uit:

var nummers = [1, 2, 3, 4, 5], totaal = 0; numbers.forEach (functie (nummer) totaal + = nummer;);

Hoewel dit geen slecht gebruik is forEachverminderen heeft nog steeds het voordeel dat we mutatie kunnen voorkomen. Met verminderen, we zouden schrijven:

var total = [1, 2, 3, 4, 5] .reduce (functie (vorige, huidige) return vorige + huidige;, 0);

Eerst bellen we verminderen op onze lijst van getallen. We geven het een callback door, die de vorige waarde en huidige waarde als argumenten accepteert en het resultaat van het samenvoegen retourneert. Sinds we zijn gepasseerd 0 als een tweede argument voor verminderen, het zal dat gebruiken als de waarde van voorgaand bij de eerste iteratie.

Als we het stap voor stap doen, ziet het er als volgt uit:

herhaling  voorgaand Stroom Totaal
1 0 1 1
2 1 2 3
3 3 3 6
4 6 4 10
5 10 5 15

Als u geen fan bent van tabellen, voert u dit fragment in de console uit:

var total = [1, 2, 3, 4, 5] .reduce (functie (vorige, huidige, index) var val = vorige + huidige; console.log ("De vorige waarde is" + vorige + "; de huidige waarde waarde is "+ current +", en de huidige iteratie is "+ (index + 1)); return val;, 0); console.log ("De lus is voltooid en de uiteindelijke waarde is" + totaal + ".");

Om samen te vatten: verminderen itereert over alle elementen van een array, maar combineert ze zoals u in uw callback opgeeft. Op elke iteratie heeft uw callback toegang tot de voorgaand waarde, welke is de total-zo-ver, of geaccumuleerde waarde; de huidige waarde; de huidige index; en het geheel rangschikking, als je ze nodig hebt.

Laten we teruggaan naar ons voorbeeld van taken. We hebben een lijst met taaknamen gekregen van kaart, en een gefilterde lijst met taken die lang duurden met ... nou, filter

Wat als we de totale hoeveelheid tijd die we vandaag werkten wilden weten??

Met behulp van een forEach loop, zou je schrijven:

var total_time = 0; tasks.forEach (functie (taak) // Het plusteken dwingt slechts // task.duration van een tekenreeks naar een getal total_time + = (+ task.duration););

Met verminderen, dat wordt:

var total_time = tasks.reduce (functie (vorige, huidige) return vorige + huidige;, 0); // Gebruik van de pijl functies var total_time = taken.reduce ((vorige, huidige) vorige + huidige);

Gemakkelijk. 

Dat is bijna alles wat er is. Bijna, omdat JavaScript ons een nog onbekende methode biedt, genaamd reduceRight. In de bovenstaande voorbeelden, verminderen begonnen bij de eerste item in de array, iterating van links naar rechts:

var array_of_arrays = [[1, 2], [3, 4], [5, 6]]; var geconcateneerd = array_of_arrays.reduce (functie (vorige, huidige) return previous.concat (current);); console.log (aaneengeschakelde); // [1, 2, 3, 4, 5, 6];

reduceRight doet hetzelfde, maar in de tegenovergestelde richting:

var array_of_arrays = [[1, 2], [3, 4], [5, 6]]; var geconcateneerd = array_of_arrays.reduceRight (functie (vorige, huidige) return previous.concat (huidig);); console.log (aaneengeschakelde); // [5, 6, 3, 4, 1, 2];

ik gebruik verminderen elke dag, maar ik heb het nooit nodig gehad reduceRight. Ik denk dat je dat waarschijnlijk ook niet zult doen. Maar in het geval dat je het ooit doet, weet je nu dat het er is.

gotchas

De drie grote gotchas met verminderen zijn:

  1. Vergeten terugkeer
  2. Een initiële waarde vergeten
  3. Verwacht een array wanneer verminderen retourneert een enkele waarde

Gelukkig zijn de eerste twee gemakkelijk te vermijden. Bepalen wat uw initiële waarde moet zijn, hangt af van wat u doet, maar u zult het snel onder de knie krijgen.

De laatste lijkt misschien een beetje vreemd. Als verminderen geeft ooit slechts één waarde terug, waarom zou u een array verwachten??

Daar zijn een paar goede redenen voor. Eerste, verminderen geeft altijd een single terug waarde, niet altijd een single aantal. Als u bijvoorbeeld een array met arrays reduceert, wordt één array geretourneerd. Als je de gewoonte hebt of arrays wilt verkleinen, is het redelijk om te verwachten dat een array met een enkel item geen speciaal geval zou zijn.

Ten tweede, als verminderen deed retourneer een array met een enkele waarde, het zou natuurlijk leuk mee spelen kaart en filter, en andere functies op matrices die u waarschijnlijk zult gebruiken. 

Implementatie

Tijd voor onze laatste blik onder de motorkap. Zoals gewoonlijk heeft Mozilla een kogelvrije polyfill om te verkleinen als je het wilt bekijken.

var reduc = function (array, callback, initial) var accumulator = initial || 0; array.forEach (functie (element) accumulator = callback (accumulator, array [i]);); terugkeer accumulator; ;

Twee dingen om op te merken, hier:

  1. Deze keer gebruikte ik de naam accumulator in plaats van voorgaand. Dit is wat je meestal in het wild zult zien.
  2. Ik wijs toe accumulator een beginwaarde, als een gebruiker er een levert, en standaard aan 0, als niet. Dit is hoe het echt is verminderen gedraagt ​​zich ook.

Het samenbrengen: kaart, filter, verkleinen en kettingbaarheid

Op dit punt ben je misschien niet dat onder de indruk. 

Eerlijk genoeg: kaart, filter, en verminderen, op zichzelf zijn niet erg interessant. 

Hun ware kracht ligt immers in hun ketenbaarheid. 

Laten we zeggen dat ik het volgende wil doen:

  1. Verzamel twee dagen aan taken.
  2. Zet de taakduur in uren om, in plaats van minuten.
  3. Filter alles wat twee uur of langer duurde.
  4. Kort samengevat.
  5. Vermenigvuldig het resultaat met een uurtarief voor facturering.
  6. Geef een opgemaakt dollarbedrag uit.

Laten we eerst onze taken definiëren voor maandag en dinsdag:

var maandag = ['naam': 'Schrijf een tutorial', 'duration': 180, 'name': 'Some web development', 'duration': 120]; var tuesday = ['name': 'Blijf die tutorial schrijven', 'duration': 240, 'name': 'Some more web development', 'duration': 180, 'name': 'A whole veel niets ',' duur ': 240]; var tasks = [maandag, dinsdag];

En nu, onze lieflijk ogende transformatie:

 var result = tasks.reduce (function (accumulator, current) return accumulator.concat (current);). map (functie (taak) return (task.duration / 60);). filter (functie (duur) return duration> = 2;). map (functie (duur) retourduur * 25;) .verlaging (functie (accumulator, huidige) return [(+ accumulator) + (+ huidige)];). map (function (dollar_amount) return '$' + dollar_amount.toFixed (2);) .red (function (formatted_dollar_amount) return formatted_dollar_amount;);

Of, beknopter:

 // Onze 2D-array samenvoegen in een enkele lijst var result = tasks.reduce ((acc, current) => acc.concat (current)) // Extraheer de taakduur en converteer minuten naar uren .map ((task) = > task.duration / 60) // Filter alle taken die minder dan twee uur hebben geduurd .filter ((duur) => duur> = 2) // Vermenigvuldig de duur van elke taak aan ons uurtarief .map ((duur) = > duration * 25) // Combineer de sommen in één dollarbedrag .reduce ((acc, current) => [(+ acc) + (+ current)]) // Converteer naar een "pretty-printed" dollarbedrag. map ((aantal) => '$' + amount.toFixed (2)) // Trek het enige element van de array uit kaart .reduce ((formatted_amount) => formatted_amount); 

Als je dit tot nu toe gedaan hebt, zou dit vrij eenvoudig moeten zijn. Er zijn echter twee stukjes gekheid om uit te leggen. 

Eerst, op regel 10, moet ik schrijven:

// Resterende rest weggelaten (functie (accumulator, huidige) return [(+ accumulator) + (+ current_];)

Twee dingen om uit te leggen:

  1. De plustekens vóór accumulator en stroom hun waarden naar getallen dwingen. Als u dit niet doet, is de retourwaarde de nogal nutteloze tekenreeks, "12510075100".
  2. Als u die som niet tussen haakjes plaatst, verminderen spuwt een enkele waarde uit, niet een array. Dat zou uiteindelijk een gooien Typefout, omdat je alleen kunt gebruiken kaart op een array! 

Het laatste dat je misschien een beetje ongemakkelijk maakt, is het laatste verminderen, namelijk:

// Resterende weggelaten kaart (function (dollar_amount) return '$' + dollar_amount.toFixed (2);) .red (function (formatted_dollar_amount) return formatatted_dollar_amount;);

Die oproep aan kaart retourneert een array met een enkele waarde. Hier bellen we verminderen om die waarde eruit te halen.

De andere manier om dit te doen zou zijn om de oproep te verwijderen en te indexeren in de array die kaart spuugt:

var result = tasks.reduce (function (accumulator, current) return accumulator.concat (current);). map (functie (taak) return (task.duration / 60);). filter (functie (duur) return duration> = 2;). map (functie (duur) retourduur * 25;) .verlaging (functie (accumulator, huidige) return [(+ accumulator) + (+ huidige)];). kaart (functie (dollar_bedrag) return '$' + dollar_amount.toFixed (2);) [0];

Dat is volkomen correct. Als u meer vertrouwd bent met het gebruik van een array-index, gaat u gewoon door.

Maar ik moedig u aan om dat niet te doen. Een van de krachtigste manieren om deze functies te gebruiken, is op het gebied van reactief programmeren, waarbij u niet vrij bent om array-indices te gebruiken. Als je die gewoonte nu aanpakt, wordt het leren van reactieve technieken veel gemakkelijker in de loop van de tijd.

Eindelijk, laten we zien hoe onze vriend de forEach loop zou het voor elkaar krijgen:

var geconcateneerd = maandag .concat (dinsdag), vergoedingen = [], formatted_sum, hourly_rate = 25, total_fee = 0; geconcateneerd.voorElk (functie (taak) var duration = task.duration / 60; if (duration> = 2) fees.push (duration * hourly_rate);); fees.forEach (functie (kosten) total_fee + = fee); var formatted_sum = '$' + total_fee.toFixed (2);

Verdraagzaam, maar lawaaierig.

Conclusie & Volgende stappen

In deze tutorial heb je geleerd hoe kaartfilter, en verminderen werk; hoe ze te gebruiken; en ongeveer hoe ze zijn geïmplementeerd. Je hebt gezien dat ze je allemaal toestaan ​​om het muteren van staat te vermijden, welke gebruik voor en forEach loops vereist, en je zou nu een goed idee moeten hebben hoe je ze allemaal kunt combineren. 

Inmiddels ben ik er zeker van dat je graag wilt oefenen en verder wilt lezen. Hier zijn mijn top drie suggesties voor waar naartoe te gaan:

  1. Jafar Husain's fantastische reeks oefeningen over functionele programmering in JavaScript, compleet met een gedegen introductie tot Rx.js
  2. Envato Tuts + Instructeur Jason Rhodes 'cursus over functionele programmering in JavaScript
  3. De meest adequate gids voor functionele programmering, die dieper ingaat op waarom we mutatie vermijden, en functioneel denken in het algemeen

JavaScript is een van de de-facto talen geworden van het werken op het web. Het is niet zonder zijn leercurven, en er zijn ook genoeg kaders en bibliotheken om je bezig te houden. Als u op zoek bent naar extra bronnen om te studeren of te gebruiken in uw werk, kijk dan wat we beschikbaar hebben op de Envato-marktplaats.

Als je meer over dit soort dingen wilt, bekijk dan mijn profiel van tijd tot tijd; vang me op Twitter (@PelekeS); of klik op mijn blog op http://peleke.me.

Vragen, opmerkingen of verwarringen? Laat ze hieronder staan ​​en ik zal mijn best doen om ze allemaal afzonderlijk te beantwoorden.

Leer JavaScript: de complete gids

We hebben een complete handleiding samengesteld om u te helpen JavaScript te leren, of u net bent begonnen als een webontwikkelaar of dat u meer geavanceerde onderwerpen wilt verkennen.