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.
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:
Laten we naar een code kijken.
kaart
in praktijkStel 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:
kaart
, je hoeft niet de staat van de te beheren voor
loop jezelf.Duwen
erin. kaart
retourneert het voltooide product in één keer, zodat we de retourwaarde eenvoudig aan een nieuwe variabele kunnen toewijzen.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:
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.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.
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!
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.
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:
filter
opfilter
in praktijkLaten 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:
forEach
of voor
lusHet 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.
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; ;
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 filter
, verminderen
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:
verminderen
opMerk 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 praktijkSinds 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 forEach
, verminderen
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.
De drie grote gotchas met verminderen
zijn:
terugkeer
verminderen
retourneert een enkele waardeGelukkig 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.
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:
accumulator
in plaats van voorgaand
. Dit is wat je meestal in het wild zult zien.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.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:
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:
accumulator
en stroom
hun waarden naar getallen dwingen. Als u dit niet doet, is de retourwaarde de nogal nutteloze tekenreeks, "12510075100"
.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.
In deze tutorial heb je geleerd hoe kaart
, filter
, 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:
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.