Fonctions d'ordre supérieur dans JS: cours pour jeunes combattants

Cet article est destiné à une personne faisant ses premiers pas timides sur le chemin épineux de l'apprentissage de JavaScript. Malgré le fait qu'en 2018, j'utilise la syntaxe ES5 pour que l'article puisse être compris par les jeunes Padawans suivant le cours JavaScript niveau 1 à HTML Academy.

L'une des caractéristiques qui distingue JS de nombreux autres langages de programmation est que dans ce langage une fonction est un «objet de première classe». Ou, en russe, une fonction est un sens. Identique à un nombre, une chaîne ou un objet. Nous pouvons écrire une fonction dans une variable, nous pouvons la mettre dans un tableau ou dans une propriété d'objet. Nous pouvons même additionner deux fonctions. En fait, rien de significatif n'en résultera, mais en fait - nous le pouvons!

function hello(){}; function world(){}; console.log(hello + world); //  ,  ,   //   ,     

La chose la plus intéressante est que nous pouvons créer des fonctions qui opèrent sur d'autres fonctions - en les acceptant comme arguments ou en les renvoyant comme valeur. Ces fonctions sont appelées fonctions d'ordre supérieur . Et aujourd'hui, nous, filles et garçons, parlerons de la manière d'adapter cette opportunité aux besoins de l'économie nationale. En cours de route, vous en apprendrez plus sur certaines fonctionnalités utiles des fonctions de JS.

Pipeline


Disons que nous avons une chose avec laquelle vous devez faire beaucoup de pièces. Disons qu'un utilisateur a téléchargé un fichier texte qui stocke des données au format JSON et que nous voulons traiter son contenu. Premièrement, nous devons couper les caractères d'espaces supplémentaires, qui pourraient "grossir" sur les bords en raison des actions de l'utilisateur ou du système d'exploitation. Vérifiez ensuite qu'il n'y a pas de code malveillant dans le texte (qui sait, ces utilisateurs). JSON.parse ensuite du texte à l'objet à l'aide de la méthode JSON.parse . Supprimez ensuite les données dont nous avons besoin de cet objet. Et à la fin - envoyez ces données au serveur. Vous obtenez quelque chose comme ça:

 function trim(){/*  */}; function sanitize(){/*  */}; function parse(){/*  */}; function extractData(){/*  */}; function send(){/*  */}; var textFromFile = getTextFromFile(); send(extractData(parse(sanitize(trim(testFromFile)))); 

Il semble si d'accord. De plus, vous n'avez probablement pas remarqué qu'il manque un support de fermeture. Bien sûr, l'EDI vous le dirait, mais il y a toujours un problème. Pour le résoudre, un nouvel opérateur |> a été récemment proposé. En fait, ce n'est pas nouveau, mais honnêtement emprunté aux langages fonctionnels, mais ce n'est pas la question. En utilisant cet opérateur, la dernière ligne pourrait être réécrite comme suit:

 textFromFile |> trim |> sanitize |> parse |> extractData |> send; 

L'opérateur |> prend son opérande gauche et le passe à l'opérande droit comme argument. Par exemple, "Hello" |> console.log équivalent à console.log("Hello") . C'est très pratique précisément pour les cas où plusieurs fonctions sont appelées le long de la chaîne. Cependant, avant l'introduction de cet opérateur, beaucoup de temps passera (si cette proposition est acceptée), mais vous devez vivre d'une manière ou d'une autre maintenant. Par conséquent, nous pouvons écrire sur notre vélo une fonction qui simule ce comportement:

 function pipe(){ var args = Array.from(arguments); var result = args.shift(); while(args.length){ var f = args.shift(); result = f(result); } return result; } pipe( textFromFile, trim, sanitize, parse, extractData, send ); 

Si vous êtes un novice javascriptist (javascript? Javascript?), La première ligne de la fonction peut vous sembler incompréhensible. C'est simple: à l'intérieur de la fonction, nous utilisons le mot-clé arguments pour accéder à un objet de type tableau contenant tous les arguments passés à la fonction. C'est très pratique lorsque nous ne savons pas à l'avance combien d'arguments elle aura. Un objet massif est comme un tableau, mais pas tout à fait. Par conséquent, nous le convertissons en un tableau normal à l'aide de la méthode Array.from . J'espère que le code supplémentaire est déjà assez lisible: nous commençons de gauche à droite pour extraire les éléments du tableau et les appliquer les uns aux autres de la même manière que le ferait l'opérateur |>.

Journalisation


Voici un autre exemple proche de la vraie vie. Supposons que nous ayons déjà une fonction f qui fait ... quelque chose d'utile. Et dans le processus de test de notre code, nous voulons en savoir plus sur exactement comment f fait. À quels moments est appelé, quels arguments lui sont passés, quelles valeurs sont renvoyées.

Bien sûr, à chaque appel de fonction, nous pouvons écrire ceci:

 var result = f(a, b); console.log(" f     " + a + "  " + b + "   " + result); console.log(" : " + Date.now()); 

Mais, premièrement, c'est assez lourd. Et deuxièmement, il est très facile de l’oublier. Un jour, nous écrirons simplement f(a, b) , et depuis lors, l'obscurité de l'ignorance s'installera dans nos esprits. Il s'élargira à chaque nouveau défi f , dont nous ne savons rien.

Idéalement, j'aimerais que la journalisation se fasse automatiquement. Pour que chaque fois que vous appelez f , toutes les choses dont nous avons besoin soient écrites sur la console. Et, heureusement, nous avons un moyen de le faire. Découvrez la nouvelle fonctionnalité d'ordre supérieur!

 function addLogger(f){ return function(){ var args = Array.from(arguments); var result = f.apply(null, args); console.log(" " + f.name + "     " + args.join() + "    " + result + "\n" + " : " + Date.now()); return result; } } function sum(a, b){ return a + b; } var sumWithLogging = addLogger(sum); sum(1, 2); //   sumWithLogging(1, 2) //  

Une fonction prend une fonction et renvoie une fonction qui appelle la fonction transmise à la fonction lors de sa création. Désolé, je ne pouvais pas arrêter d'écrire ceci. Maintenant en russe: la fonction addLogger crée un addLogger autour de la fonction qui lui est passée en argument. L'emballage est également une fonction. Lorsqu'il est appelé, il collecte un tableau de ses arguments de la même manière que dans l'exemple précédent. Ensuite, à l'aide de la méthode apply , il appelle une fonction encapsulée avec les mêmes arguments et se souvient du résultat. Après cela, l'encapsuleur écrit tout sur la console.

Ici, nous avons le cas classique de l'attaque de l'homme au milieu. Si vous utilisez un wrapper au lieu de f , alors du point de vue du code qui l'utilise, il n'y a pratiquement aucune différence. Le code peut supposer qu'il communique directement avec f . Pendant ce temps, le wrapper rapporte tout au camarade major.

Eins, zwei, drei, vier ...


Et encore une tâche proche de la pratique. Supposons que nous devions numéroter certaines entités. Chaque fois qu'une nouvelle entité apparaît, nous obtenons un nouveau numéro pour elle, un de plus que le précédent. Pour ce faire, nous démarrons une fonction de la forme suivante:

 var lastNumber = 0; function getNewNumber(){ return lastNumber++; } 

Et puis nous avons un nouveau type d'entité. Disons, avant cela, nous avons numéroté les lapins, et maintenant il y a aussi des lapins. Si vous utilisez une fonction pour ceux-ci et pour les autres, alors chaque numéro attribué aux lapins fera un «trou» dans la série de numéros délivrés aux lapins. Donc, nous avons besoin de la deuxième fonction, et avec elle la deuxième variable:

 var lastHareNumber = 0; function getNewHareNumber(){ return lastHareNumber++; } var lastRabbitNumber = 0; function getNewRabbitNumber(){ return lastRabbitNumber++; } 

Vous sentez que ce code sent mauvais? J'aimerais avoir quelque chose de mieux. Premièrement, je voudrais pouvoir déclarer de telles fonctions moins verbeuses, sans dupliquer le code. Et deuxièmement, je voudrais en quelque sorte "empaqueter" la variable que la fonction utilise dans la fonction elle-même afin de ne pas obstruer à nouveau l'espace de noms.

Et puis un homme fait irruption, familiarisé avec le concept de POO, et dit:
"Élémentaire, Watson." Il est nécessaire de faire des générateurs de nombres non pas des objets, mais des objets. Les objets sont juste conçus pour stocker des fonctions qui fonctionnent avec des données, avec ces mêmes données. Ensuite, nous pourrions écrire quelque chose comme:

 var numberGenerator = new NumberGenerator(); var n = numberGenerator.get(); 

A quoi je répondrai:
- Pour être honnête, je suis entièrement d'accord avec vous. Et en principe, c'est une approche plus correcte que ce que je vais maintenant proposer. Mais ici, nous avons un article sur les fonctions, pas sur la POO. Pourriez-vous vous taire un moment et me laisser finir?

Ici (surprise!) La fonction d'ordre supérieur nous aidera à nouveau.

 function createNumberGenerator(){ var n = 0; return function(){ return n++; } } var getNewHareNumber = createNumberGenerator(); var getNewRabbitNumber = createNumberGenerator(); console.log( getNewHareNumber(), getNewHareNumber(), getNewHareNumber(), getNewRabbitNumber(), getNewRabbitNumber(), ); //    0, 1, 2, 0, 1 

Et ici, certaines personnes peuvent avoir une question, peut-être même sous une forme obscène: que diable se passe-t-il? Pourquoi créons-nous une variable qui n'est pas utilisée dans la fonction elle-même? Comment une fonction interne y accède-t-elle si une fonction externe a terminé son exécution depuis longtemps? Pourquoi deux fonctions créées faisant référence à la même variable obtiennent-elles des résultats différents? Une réponse à toutes ces questions est la clôture .

Chaque fois que la fonction createNumberGenerator est createNumberGenerator , l'interpréteur JS crée une chose magique appelée "contexte d'exécution". En gros, c'est un tel objet dans lequel toutes les variables déclarées dans cette fonction sont stockées. Nous ne pouvons pas y accéder en tant qu'objet javascript ordinaire, mais c'est néanmoins le cas.

Si la fonction était «simple» (par exemple, en ajoutant des nombres), alors après la fin de son travail, le contexte d'exécution s'avère inutile. Savez-vous ce qui arrive aux données inutiles dans JS? Ils sont dévorés par un démon insatiable nommé Garbage Collector. Cependant, si la fonction était «compliquée», il peut arriver que quelqu'un ait toujours besoin de son contexte même après l'exécution de cette fonction. Dans ce cas, le Garbage Collector l'épargne, et il reste suspendu quelque part dans sa mémoire, afin que ceux qui en ont besoin puissent toujours avoir accès à lui.

Ainsi, la fonction retournée par createNumberGenerator aura toujours accès à sa propre copie de la variable n . Vous pouvez le considérer comme un sac de tenue de D&D. Vous mettez votre main dans votre sac et vous vous retrouvez dans une «poche» interdimensionnelle personnelle où vous pouvez ranger tout ce que vous voulez.

Debounce


Il y a une chose telle que «éliminer le rebond». C'est lorsque nous ne voulons pas qu'une fonction soit appelée trop souvent. Supposons qu'il existe un certain bouton, en cliquant sur celui qui lance un processus «coûteux» (long, ou consommant beaucoup de mémoire, ou Internet, ou sacrifiant des vierges). Il peut arriver qu'un utilisateur impatient commence à cliquer sur ce bouton avec une fréquence de plus de dix hertz. De plus, le processus susmentionné a une nature telle qu'il est inutile de l'exécuter dix fois de suite, car le résultat final ne changera pas. C'est alors que nous appliquons «l'élimination des bavardages».

Son essence est très simple: nous effectuons la fonction non pas immédiatement, mais après un certain temps. Si avant que ce temps ne soit écoulé, la fonction a été rappelée, nous «remettons à zéro la minuterie». Ainsi, l'utilisateur peut cliquer sur le bouton au moins mille fois - une seule est nécessaire pour le sacrifice. Cependant, moins de mots, plus de code:

 function debounce(f, delay){ var lastTimeout; return function(){ if(lastTimeout){ clearTimeout(lastTimeout); } var args = Array.from(arguments); lastTimeout = setTimeout(function(){ f.apply(null, args); }, delay); } } function sacrifice(name){ console.log(name + "     * *"); } function sacrificeDebounced = debounce(sacrifice, 500); sacrificeDebounced(""); sacrificeDebounced(""); sacrificeDebounced(""); 

Dans une demi-seconde, Lena sera sacrifiée, et Katya et Sveta survivront grâce à notre fonction magique.

Si vous lisez attentivement les exemples précédents, vous devez bien comprendre comment tout fonctionne ici. La fonction wrapper créée par debounce déclenche l'exécution différée de la fonction d'origine à l'aide de setTimeout . Dans ce cas, l'identifiant de délai d'attente est stocké dans la variable lastTimeout, qui est accessible à l'encapsuleur en raison de la fermeture. Si l'identificateur de délai d'attente est déjà dans cette variable, l'encapsuleur annule ce délai d'expiration avec clearTimeout . Si le délai d'attente précédent est déjà terminé, rien ne se passe. Sinon, tant pis pour lui.

Sur ce point, je terminerai peut-être. J'espère qu'aujourd'hui vous avez appris beaucoup de nouvelles choses, et surtout - vous avez compris tout ce que vous avez appris. A bientôt.

Source: https://habr.com/ru/post/fr421537/


All Articles