Comment utiliser la carte, filtrer et réduire en JavaScript

La programmation fonctionnelle fait actuellement des vagues dans le monde du développement. Et pour cause: les techniques fonctionnelles peuvent vous aider à écrire davantage de code déclaratif, plus facile à comprendre d'un coup d'œil, à refactoriser et à tester.. 

L’une des pierres angulaires de la programmation fonctionnelle est son utilisation particulière des listes et de leurs opérations. Et ces choses sont exactement ce que le son ressemble: tableaux de choses et tout ce que vous leur faites. Mais la mentalité fonctionnelle les traite un peu différemment de ce à quoi on pourrait s'attendre.

Cet article examinera de près ce que j'aime appeler les "trois grandes" opérations de liste: carte, filtre, et réduire. Envisager ces trois fonctions en tête est une étape importante pour pouvoir écrire du code fonctionnel propre et ouvrir les portes aux techniques extrêmement puissantes de la programmation fonctionnelle et réactive..

Cela signifie également que vous ne devrez jamais écrire un pour boucle encore.

Curieuse? Plongeons dedans.

Une carte de liste en liste

Souvent, nous avons besoin de prendre un tableau et de modifier chaque élément de celui-ci de la même manière. Des exemples typiques en sont la quadrature de chaque élément d'un tableau de nombres, l'extraction du nom d'une liste d'utilisateurs ou l'exécution d'une expression régulière sur un tableau de chaînes..

carte est une méthode conçue pour faire exactement cela. C'est défini sur Array.prototype, afin que vous puissiez l'appeler sur n'importe quel tableau, et il accepte un rappel comme premier argument. 

Quand vous appelez carte sur un tableau, il exécute ce rappel sur tous ses éléments, renvoyant un Nouveau tableau avec toutes les valeurs renvoyées par le rappel.

Sous la capuche, carte passe trois arguments à votre rappel:

  1. le article actuel dans le tableau
  2. le index de tableau de l'élément en cours
  3. le tableau entier vous avez appelé la carte sur 

Regardons du code.

carte en pratique

Supposons que nous ayons une application qui gère un tableau de vos tâches pour la journée. Chaque tâche est un objet, chacun avec un prénom et durée propriété:

// Les durées sont en minutes var tasks = ['nom': 'Écrire pour Envato Tuts +', 'durée': 120, 'nom': 'Entraînement', 'durée': 60, 'nom' : 'Tergiverser sur Duolingo', 'durée': 240];

Supposons que nous voulions créer un nouveau tableau avec juste le nom de chaque tâche afin de pouvoir examiner tout ce que nous avons accompli aujourd'hui. Utilisant un pour boucle, nous écririons quelque chose comme ceci:

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

JavaScript offre également une pour chaque boucle. Il fonctionne comme un pour boucle, mais gère tout le désordre de vérifier notre index de boucle par rapport à la longueur du tableau pour nous:

var task_names = []; tasks.forEach (function (task) nom_tâche.push (task.name););

En utilisant carte, nous pouvons écrire:

var nom_tâche = tasks.map (fonction (tâche, index, tableau) return task.name;);

J'inclus le indice et  tableau paramètres pour vous rappeler qu'ils sont là si vous en avez besoin. Puisque je ne les ai pas utilisés ici, vous pouvez les laisser de côté et le code fonctionnerait très bien..

Il existe quelques différences importantes entre les deux approches:

  1. En utilisant carte, vous n'avez pas à gérer l'état de la pour boucle toi.
  2. Vous pouvez agir sur l'élément directement, sans avoir à indexer dans le tableau.
  3. Vous n'êtes pas obligé de créer un nouveau tableau et pousser dans ça. carte renvoie le produit fini en une seule fois, nous pouvons donc simplement affecter la valeur de retour à une nouvelle variable.
  4. Vous faire ne pas oublier d'inclure un revenir déclaration dans votre rappel. Si vous ne le faites pas, vous aurez un nouveau tableau rempli avec indéfini

Il s'avère que, tout des fonctions que nous examinerons aujourd'hui partagent ces caractéristiques.

Le fait que nous n'ayons pas à gérer manuellement l'état de la boucle rend notre code plus simple et plus facile à gérer. Le fait que nous puissions agir directement sur l'élément au lieu d'avoir à indexer dans le tableau rend les choses plus lisibles.. 

Utilisant un pour chaque boucle résout ces deux problèmes pour nous. Mais carte a toujours au moins deux avantages distincts:

  1. pour chaque résultats indéfini, de sorte qu'il ne chaîne avec d'autres méthodes de tableau. carte retourne un tableau, donc vous pouvez enchaîner avec d'autres méthodes de tableau.
  2. carte résultatsun tableau avec le produit fini, plutôt que de nous obliger à muter un tableau à l'intérieur de la boucle. 

Garder le nombre de places où vous modifiez l'état à un minimum absolu est un principe important de la programmation fonctionnelle. Cela rend le code plus sûr et plus intelligible.

C’est également le bon moment pour signaler que si vous êtes dans Node, testez ces exemples dans la console du navigateur Firefox, ou utilisez Babel ou Traceur, vous pouvez écrire ceci plus précisément avec les fonctions de flèche ES6:

var task_names = tasks.map ((task) => task.name);

Les fonctions fléchées nous laissent de côté les revenir mot clé en une ligne. 

Ça ne devient pas beaucoup plus lisible que ça.

Gotchas

Le rappel que vous passez à carte doit avoir un explicite revenir déclaration, ou carte va cracher un tableau plein de indéfini. Il n’est pas difficile de se rappeler d’inclure un revenir valeur, mais il n'est pas difficile d'oublier. 

Si vous faire oublier, carte ne va pas se plaindre. Au lieu de cela, il restituera discrètement un tableau rempli de rien. De telles erreurs silencieuses peuvent être étonnamment difficiles à résoudre. 

Heureusement, c'est le seulement eu avec carte. Mais c’est un piège assez commun que je dois souligner: assurez-vous toujours que votre rappel contient un revenir déclaration!

la mise en oeuvre

Lire des implémentations est une partie importante de la compréhension. Alors, écrivons notre propre poids léger carte pour mieux comprendre ce qui se passe sous le capot. Si vous souhaitez voir une implémentation de qualité production, consultez le polyfill de Mozilla sur MDN.

var map = fonction (tableau, rappel) var new_array = []; array.forEach (function (élément, index, tableau) new_array.push (callback (element));); return new_array; ; var nom_tâche = map (tâches, fonction (tâche) return task.name;);

Ce code accepte un tableau et une fonction de rappel comme arguments. Il crée ensuite un nouveau tableau. exécute le rappel sur chaque élément du tableau que nous avons transmis; pousse les résultats dans le nouveau tableau; et retourne le nouveau tableau. Si vous l'exécutez dans votre console, vous obtiendrez le même résultat qu'auparavant. Assurez-vous simplement d'initialiser les tâches avant de le tester!

Pendant que nous utilisons une boucle for sous le capot, l'envelopper dans une fonction cache les détails et nous permet de travailler avec l'abstraction à la place.. 

Cela rend notre code plus déclaratif, dit-il quoi faire, pas Comment pour le faire. Vous apprécierez combien plus lisible, maintenable et erm, débogable cela peut rendre votre code.

Filtrer le bruit

La prochaine de nos opérations de tableau est filtre. Cela fait exactement ce que cela ressemble: il faut un tableau, et filtre les éléments indésirables.

Comme carte, filtre est défini sur Array.prototype. Il est disponible sur n'importe quel tableau, et vous lui passez un rappel comme premier argument. filtre exécute ce rappel sur chaque élément du tableau et crache un Nouveau tableau contenant seulement les éléments pour lesquels le rappel a été renvoyé vrai.

Aussi comme carte, filtre passe vos trois arguments de rappel:

  1. le article actuel 
  2. le index actuel
  3. le tableau vous avez appelé filtre sur

filtre en pratique

Revenons sur notre exemple de tâche. Au lieu de tirer les noms de chaque tâche, supposons que je veuille obtenir une liste des tâches qui m'ont pris deux heures ou plus.. 

En utilisant pour chaque, nous écririons:

var difficult_tasks = []; tasks.forEach (fonction (tâche) if (tâche.duration> = 120) difficulté_tasks.push (tâche););

Avec filtre:

var difficult_tasks = tasks.filter (fonction (tâche) retour tâche.duration> = 120;); // Utiliser ES6 var difficult_tasks = tasks.filter ((task) => task.duration> = 120);

Ici, je suis allé de l'avant et laissé de côté le indice et tableau arguments à notre rappel, puisque nous ne les utilisons pas.

Juste comme carte, filtre nous permet:

  • éviter de muter un tableau à l'intérieur d'un pour chaque ou pour boucle
  • assigner son résultat directement à une nouvelle variable, plutôt que de pousser dans un tableau défini ailleurs

Gotchas

Le rappel que vous passez à carte doit inclure une instruction return si vous voulez qu'elle fonctionne correctement. Avec filtre, vous devez également inclure une déclaration de retour, et vous doit assurez-vous qu'il renvoie une valeur booléenne.

Si vous oubliez votre déclaration, votre rappel vous sera renvoyé. indéfini, lequel filtre contraindra inutilement à faux. Au lieu de générer une erreur, il retournera en silence un tableau vide! 

Si vous suivez l’autre itinéraire et retournez quelque chose qui est n'est pas explicitement vrai ou faux, puis filtre essaiera de comprendre ce que vous vouliez dire en appliquant les règles de coercition de JavaScript. Plus souvent qu'autrement, c'est un bug. Et, tout comme oublier votre déclaration de retour, ce sera une déclaration silencieuse. 

Toujours assurez-vous que vos rappels incluent une déclaration de retour explicite. Et toujours assurez-vous que vos rappels dans filtre revenir vrai ou faux. Votre santé mentale va vous remercier.

la mise en oeuvre

Encore une fois, la meilleure façon de comprendre un morceau de code est de… l'écrire. Roulons notre propre poids léger filtre. Les bonnes personnes de Mozilla ont également un polyfill de force industrielle à lire..

var filtre = fonction (tableau, rappel) var filtré_array = []; array.forEach (function (élément, index, tableau) if (rappel (élément, index, tableau)) filtré_array.push (élément);); return filter_array; ;

Réduire les tableaux

carte crée un nouveau tableau en transformant chaque élément d'un tableau, individuellement. filtre crée un nouveau tableau en supprimant les éléments qui n'appartiennent pas. réduire, d'autre part, prend tous les éléments d'un tableau, et réduit les en une seule valeur.

Juste comme carte et filtreréduire est défini sur Array.prototype et ainsi disponible sur n'importe quel tableau, et vous passez un rappel comme premier argument. Mais il faut aussi un deuxième argument optionnel: la valeur pour commencer à combiner tous vos éléments de tableau dans. 

réduire passe vos quatre arguments de rappel:

  1. le valeur actuelle
  2. le valeur précédente
  3. le index actuel
  4. le tableau vous avez appelé réduire sur

Notez que le rappel obtient un valeur précédente à chaque itération. A la première itération, il y a est pas de valeur précédente. C’est pourquoi vous avez la possibilité de passer réduire une valeur initiale: Il s'agit de la "valeur précédente" pour la première itération, sinon il n'y en aurait pas.

Enfin, gardez à l’esprit que réduire renvoie un valeur unique, ne pas un tableau contenant un seul élément. Ceci est plus important qu'il n'y parait, et j'y reviendrai dans les exemples.

réduire en pratique

Puisque réduire est la fonction que les gens trouvent le plus extra-terrestre au début, nous allons commencer par une étape à la fois, à travers quelque chose de simple.

Disons que nous voulons trouver la somme d'une liste de nombres. En utilisant une boucle, cela ressemble à ceci:

nombres var = [1, 2, 3, 4, 5], total = 0; nombres.pour chaque (fonction (nombre) total + = nombre;);

Bien que ce ne soit pas un mauvais cas d’utilisation pour pour chaqueréduire a toujours l'avantage de nous permettre d'éviter la mutation. Avec réduire, nous écririons:

var total = [1, 2, 3, 4, 5] .reduce (function (précédent, actuel) retour précédent + actuel;, 0);

Tout d'abord, nous appelons réduire sur notre liste de Nombres. Nous lui transmettons un rappel, qui accepte la valeur précédente et la valeur actuelle comme arguments et renvoie le résultat de leur addition. Depuis que nous avons passé 0 comme second argument pour réduire, ça va l'utiliser comme valeur de précédent à la première itération.

Si on le prend pas à pas, ça ressemble à ça:

Itération  précédent Actuel Total
1 0 1 1
2 1 2 3
3 3 3 6
4 6 4 dix
5 dix 5 15

Si vous n'êtes pas un fan de tables, lancez cet extrait de code dans la console:

var total = [1, 2, 3, 4, 5] .reduce (fonction (previous, actuelle, index) var val = précédente + actuelle; console.log ("La valeur précédente est" + précédente + "; la valeur actuelle la valeur est "+ current +" et l'itération actuelle est "+ (index + 1)); return val;, 0); console.log ("La boucle est terminée et la valeur finale est" + total + ".");

Récapituler: réduire itère sur tous les éléments d'un tableau, en les combinant comme vous le spécifiez dans votre rappel. A chaque itération, votre rappel a accès au précédent valeur, qui est le total jusqu'ici, ou valeur accumulée; la valeur actuelle; la indice actuel; et l'ensemble tableau, si vous en avez besoin.

Revenons à notre exemple de tâches. Nous avons reçu une liste de noms de tâches de carte, et une liste filtrée de tâches qui a pris beaucoup de temps avec… bien, filtre

Et si nous voulions connaître le temps total passé à travailler aujourd'hui?

Utilisant un pour chaque boucle, vous écririez:

var total_time = 0; tasks.forEach (function (task) // Le signe plus ne fait que coercer // task.duration d'une chaîne en un nombre total_time + = (+ task.duration););

Avec réduire, cela devient:

var total_time = tasks.reduce (function (previous, current) retour previous + current;, 0); // Utilisation des fonctions de flèche var total_time = tasks.reduce ((previous, current) previous + current);

Facile. 

C'est presque tout ce qu'il y a à faire. Presque, parce que JavaScript nous fournit une autre méthode peu connue, appelée réduire à droite. Dans les exemples ci-dessus, réduire commencé au premier élément du tableau, itérant de gauche à droite:

var array_of_arrays = [[1, 2], [3, 4], [5, 6]]; var concatenated = array_of_arrays.reduce (function (previous, current) return previous.concat (current);); console.log (concaténé); // [1, 2, 3, 4, 5, 6];

réduire à droite fait la même chose, mais dans le sens opposé:

var array_of_arrays = [[1, 2], [3, 4], [5, 6]]; var concatenated = array_of_arrays.reduceRight (function (previous, current) return previous.concat (current);); console.log (concaténé); // [5, 6, 3, 4, 1, 2];

j'utilise réduire tous les jours, mais je n'en ai jamais eu besoin réduire à droite. Je pense que vous ne le ferez probablement pas non plus. Mais si jamais vous le faites, maintenant vous savez que c'est là.

Gotchas

Les trois gros coups avec réduire sont:

  1. Oublier de revenir
  2. Oublier une valeur initiale
  3. S'attendant à un tableau quand réduire renvoie une valeur unique

Heureusement, les deux premiers sont faciles à éviter. Le choix de votre valeur initiale dépend de ce que vous faites, mais vous vous en rendrez vite compte..

Le dernier peut sembler un peu étrange. Si réduire ne renvoie jamais qu'une seule valeur, pourquoi vous attendriez-vous à un tableau??

Il y a quelques bonnes raisons pour cela. Premier, réduire renvoie toujours un seul valeur, pas toujours un seul nombre. Si vous réduisez un tableau de tableaux, par exemple, un seul tableau sera renvoyé. Si vous avez l'habitude de réduire les tableaux, il serait juste de s'attendre à ce qu'un tableau contenant un seul élément ne soit pas un cas particulier..

Deuxièmement, si réduire fait renvoyer un tableau avec une valeur unique, il jouerait naturellement bien avec carte et filtre, et d'autres fonctions sur les tableaux que vous êtes susceptible d'utiliser avec elle. 

la mise en oeuvre

Temps pour notre dernier coup d'oeil sous le capot. Comme d'habitude, Mozilla a un polyfill à l'épreuve des balles pour réduire si vous voulez le vérifier.

var reduction = function (tableau, rappel, initial) var accumulator = initial || 0; array.forEach (function (element) accumulator = callback (accumulator, array [i]);); retour accumulateur; ;

Deux choses à noter, ici:

  1. Cette fois, j'ai utilisé le nom accumulateur au lieu de précédent. C'est ce que vous verrez habituellement à l'état sauvage.
  2. J'assigne accumulateur une valeur initiale, si un utilisateur en fournit une, et par défaut à 0, si non. Voici comment le réel réduire se comporte aussi.

Assemblage: cartographier, filtrer, réduire et mettre en chaîne

À ce stade, vous pourriez ne pas être cette impressionné. 

C'est suffisant: carte, filtre, et réduire, seuls, ne sont pas terriblement intéressants. 

Après tout, leur véritable pouvoir réside dans leur aptitude à la chaîne. 

Disons que je veux faire ce qui suit:

  1. Recueillir deux jours de tâches.
  2. Convertir les durées de tâches en heures, au lieu de minutes.
  3. Filtrer tout ce qui a pris deux heures ou plus.
  4. Résumer le tout.
  5. Multipliez le résultat par un tarif horaire pour la facturation.
  6. Produire un montant en dollars formaté.

Premièrement, définissons nos tâches pour lundi et mardi:

var monday = ['nom': 'écrire un tutoriel', 'durée': 180, 'nom': 'développement sur le web', 'durée': 120]; var mardi = ['nom': 'Continuez à écrire ce tutoriel', 'durée': 240, 'nom': 'Encore du développement web', 'durée': 180, 'nom':: Un ensemble beaucoup de rien ',' duration ': 240]; tâches var = = [lundi, mardi];

Et maintenant, notre belle transformation:

 var result = tasks.reduce (function (accumulateur, actuel) return accumulator.concat (actuel);). map (fonction (tâche) return (task.duration / 60);). filter (fonction (durée) durée de retour> = 2;). carte (fonction (durée) durée de retour * 25;). réduction (fonction (accumulateur, courant) retour [(+ accumulateur) + (+ courant)];). map (function (dollar_amount) return '$' + dollar_amount.toFixed (2);).

Ou, plus concement:

 // Concaténer notre tableau 2D en une liste unique var result = tasks.reduce ((acc, current) => acc.concat (current)) // Extrait la durée de la tâche et convertit les minutes en heures .map ((task) = > task.duration / 60) // Filtre toutes les tâches ayant pris moins de deux heures .filter ((duration) => duration> = 2) // Multiplie la durée de chaque tâche par notre tarif horaire .map ((duration) = > duration * 25) // Combinez les sommes en un montant en dollars .reduce ((acc, current) => [(+ acc) + (+ current)]) // Convertissez un montant en dollars "joliment imprimé". map ((montant) => '$' + montant.toFixé (2)) // Extrait le seul élément du tableau obtenu à partir de map .reduce ((formated_amount) => formated_amount); 

Si vous en êtes arrivé là, cela devrait être assez simple. Il y a deux morceaux d'étrangeté à expliquer, bien que. 

Premièrement, à la ligne 10, je dois écrire:

// Reste omis réduction (fonction (accumulateur, courant) return [(+ accumulateur) + + (+ current_];)

Deux choses à expliquer ici:

  1. Les signes plus devant accumulateur et actuel contraindre leurs valeurs aux nombres. Si vous ne le faites pas, la valeur de retour sera la chaîne plutôt inutile, "12510075100".
  2. Si vous ne mettez pas cette somme entre parenthèses, réduire va cracher une seule valeur, ne pas un tableau. Cela finirait par jeter un Erreur-type, parce que vous ne pouvez utiliser carte sur un tableau! 

Le deuxième élément qui peut vous rendre un peu inconfortable est le dernier réduire, à savoir:

// Carte restante omise (function (dollar_amount) return '$' + dollar_amount.toFixed (2);).

Cet appel à carte renvoie un tableau contenant une seule valeur. Ici, nous appelons réduire tirer cette valeur.

Pour ce faire, l’autre solution consiste à supprimer l’appel à réduire et à indexer dans le tableau carte Crache:

var result = tasks.reduce (function (accumulateur, actuel) return accumulator.concat (actuel);). map (fonction (tâche) return (task.duration / 60);). filter (fonction (durée) durée de retour> = 2;). carte (fonction (durée) durée de retour * 25;). réduction (fonction (accumulateur, courant) retour [(+ accumulateur) + (+ courant)];). map (fonction (dollar_amount) return '$' + dollar_amount.toFixed (2);) [0];

C'est parfaitement correct. Si vous êtes plus à l'aise avec un tableau, allez-y.

Mais je vous encourage à ne pas. L'un des moyens les plus puissants d'utiliser ces fonctions est dans le domaine de la programmation réactive, où vous ne serez pas libre d'utiliser des index de tableaux. Abandonner cette habitude maintenant facilitera l'apprentissage des techniques réactives.

Enfin, voyons comment notre ami le pour chaque la boucle le ferait:

var concatenated = lundi.concat (mardi), fees = [], formaté_sum, hourly_rate = 25, total_fee = 0; concatenated.forEach (fonction (tâche) var durée = tâche.duration / 60; si (durée> = 2) fees.push (duration * hourly_rate);); fees.forEach (fonction (fee) total_fee + = fee); var formated_sum = '$' + total_fee.toFixed (2);

Tolérable, mais bruyant.

Conclusion et prochaines étapes

Dans ce tutoriel, vous avez appris comment cartefiltre, et réduire travail; comment les utiliser et à peu près comment ils sont mis en œuvre. Vous avez vu qu'ils vous permettent tous d'éviter l'état de mutation, qui en utilisant pour et pour chaque boucles nécessite, et vous devriez maintenant avoir une bonne idée de la façon de les chaîner tous ensemble. 

À présent, je suis sûr que vous avez hâte de vous entraîner et de lire davantage. Voici mes trois principales suggestions pour savoir où aller ensuite:

  1. Le superbe ensemble d'exercices de Jafar Husain sur la programmation fonctionnelle en JavaScript, complété par une solide introduction à Rx.js
  2. Envato Tuts + cours de l'instructeur Jason Rhodes sur la programmation fonctionnelle en JavaScript
  3. Le guide le plus approprié de la programmation fonctionnelle, qui explique plus en détail pourquoi nous évitons les mutations et la pensée fonctionnelle en général

JavaScript est devenu l'un des langages de facto du travail sur le Web. Ce n’est pas sans ses courbes d’apprentissage, et il existe de nombreux cadres et bibliothèques pour vous tenir occupé. Si vous recherchez des ressources supplémentaires à étudier ou à utiliser dans votre travail, consultez ce que nous avons à votre disposition sur le marché Envato..

Si vous voulez plus sur ce genre de chose, vérifiez mon profil de temps en temps; attrape-moi sur Twitter (@PelekeS); ou frapper mon blog à http://peleke.me.

Des questions, des commentaires ou des confusions? Laissez-les ci-dessous et je ferai de mon mieux pour revenir à chacun individuellement.

Apprendre JavaScript: le guide complet

Nous avons créé un guide complet pour vous aider à apprendre le JavaScript, que vous soyez débutant en tant que développeur Web ou que vous souhaitiez explorer des sujets plus avancés..