Secrets des fonctions JavaScript

Chaque programmeur connaît les fonctions. Les fonctions en JavaScript ont de nombreuses possibilités, ce qui nous permet de les appeler "fonctions d'ordre supérieur". Mais même si vous utilisez constamment les fonctionnalités JavaScript, elles peuvent vous surprendre. Dans cet article, je couvrirai certaines des fonctionnalités avancées des fonctions JavaScript. J'espère que vous trouverez utile ce que vous apprenez aujourd'hui.











Fonctions pures



Une fonction qui répond aux deux exigences suivantes est appelée pure:



  • Il renvoie toujours le même résultat lorsqu'il est appelé avec les mêmes arguments.
  • Aucun effet secondaire ne se produit lorsque la fonction est exécutée.


Prenons un exemple:



function circleArea(radius){
  return radius * radius * 3.14
}


Si cette fonction reçoit la même valeur radius, elle renvoie toujours le même résultat. Dans le même temps, lors de l'exécution de la fonction, rien en dehors ne change, c'est-à-dire qu'il n'a pas d'effets secondaires. Tout cela signifie que c'est une fonction pure.



Voici un autre exemple:



let counter = (function(){
  let initValue = 0
  return function(){
    initValue++;
    return initValue
  }
})()


Essayons cette fonction dans la console du navigateur.





Tester la fonction dans la console du navigateur



Comme vous pouvez le voir, la fonctioncounterqui implémente le compteur renvoie des résultats différents à chaque fois qu'elle est appelée. Par conséquent, il ne peut pas être appelé pur.



Et voici un autre exemple:



let femaleCounter = 0;
let maleCounter = 0;
function isMale(user){
  if(user.sex = 'man'){
    maleCounter++;
    return true
  }
  return false
}


Voici une fonction isMalequi, lorsqu'elle est passée avec le même argument, renvoie toujours le même résultat. Mais cela a des effets secondaires. À savoir, nous parlons de changer une variable globale maleCounter. Par conséquent, cette fonction ne peut pas être appelée pure.



▍Pourquoi des fonctions pures sont-elles nécessaires?



Pourquoi tracer la ligne entre les fonctions régulières et pures? Le fait est que les fonctions pures ont de nombreux atouts. Leur utilisation peut améliorer la qualité du code. Parlons de ce que nous donne l'utilisation de fonctions pures.



1. Le code des fonctions pures est plus clair que le code des fonctions ordinaires, il est plus facile à lire



Chaque fonction pure vise une tâche spécifique. Il, appelé avec la même entrée, renvoie toujours le même résultat. Cela améliore considérablement la lisibilité du code et facilite sa documentation.



2. Les fonctions pures se prêtent mieux à l'optimisation lors de la compilation de leur code



Supposons que vous ayez un morceau de code comme celui-ci:



for (int i = 0; i < 1000; i++){
    console.log(fun(10));
}


Si fun- c'est une fonction qui n'est pas pure, alors pendant l'exécution de ce code, cette fonction devra être appelée fun(10)1000 fois.



Et s'il funs'agit d'une fonction pure, le compilateur peut optimiser le code. Cela pourrait ressembler à ceci:



let result = fun(10)
for (int i = 0; i < 1000; i++){
    console.log(result);
}


3. Les fonctions pures sont plus faciles à tester



Les tests fonctionnels purs ne doivent pas être sensibles au contexte. Lors de l'écriture de tests unitaires pour des fonctions pures, ils transmettent simplement certaines valeurs d'entrée à ces fonctions et vérifient ce qu'elles renvoient par rapport à certaines exigences.



Voici un exemple simple. La fonction pure prend un tableau de nombres comme argument et ajoute 1 à chaque élément de ce tableau, retournant un nouveau tableau. Voici une représentation abrégée:



const incrementNumbers = function(numbers){
  // ...
}


Pour tester une telle fonction, il suffit d'écrire un test unitaire qui ressemble à ce qui suit:



let list = [1, 2, 3, 4, 5];
assert.equals(incrementNumbers(list), [2, 3, 4, 5, 6])


Si une fonction n'est pas pure, pour la tester, vous devez prendre en compte de nombreux facteurs externes pouvant affecter son comportement. En conséquence, tester une telle fonction sera plus difficile que tester une fonction pure.



Fonctions d'ordre supérieur.



Une fonction d'ordre supérieur est une fonction qui possède au moins l'une des capacités suivantes:



  • Il est capable d'accepter d'autres fonctions comme arguments.
  • Il peut renvoyer une fonction suite à son travail.


L'utilisation de fonctions d'ordre supérieur vous permet d'augmenter la flexibilité de votre code, vous aidant à écrire des programmes plus compacts et efficaces.



Disons qu'il existe un tableau d'entiers. Il faut créer sur sa base un nouveau tableau de même longueur, mais tel, dont chaque élément représentera le résultat de la multiplication par deux de l'élément correspondant du tableau d'origine.



Si vous n'utilisez pas les capacités des fonctions d'ordre supérieur, la solution à ce problème peut ressembler à ceci:



const arr1 = [1, 2, 3];
const arr2 = [];
for (let i = 0; i < arr1.length; i++) {
    arr2.push(arr1[i] * 2);
}


Si vous pensez au problème, il s'avère que les objets de type Arrayen JavaScript ont une méthode map(). Cette méthode est appelée comme map(callback). Il crée un nouveau tableau rempli des éléments du tableau pour lequel il est appelé, traité avec la fonction qui lui est passée callback.



Voici à quoi ressemble la solution à ce problème en utilisant la méthode map():



const arr1 = [1, 2, 3];
const arr2 = arr1.map(function(item) {
  return item * 2;
});
console.log(arr2);


La méthode map()est un exemple de fonction d'ordre supérieur.



Une utilisation correcte des fonctions d'ordre supérieur contribue à améliorer la qualité de votre code. Dans les sections suivantes de ce matériel, nous reviendrons sur ces fonctions plus d'une fois.



Résultats de la fonction de mise en cache



Disons que vous avez une fonction pure qui ressemble à ceci:



function computed(str) {    
    // ,       
    console.log('2000s have passed')
      
    // ,     
    return 'a result'
}


Afin d'améliorer les performances du code, il ne nous fera pas de mal de recourir à la mise en cache des résultats des calculs effectués dans la fonction. Lorsque vous appelez une telle fonction avec les mêmes paramètres avec lesquels elle a déjà été appelée, vous n'aurez pas à effectuer à nouveau les mêmes calculs. Au lieu de cela, leurs résultats, précédemment stockés dans le cache, seront renvoyés immédiatement.



Comment équiper une fonction d'un cache? Pour ce faire, vous pouvez écrire une fonction spéciale qui peut être utilisée comme wrapper pour la fonction cible. Nous donnerons un nom à cette fonction spéciale cached. Cette fonction prend une fonction objectif comme argument et renvoie une nouvelle fonction. Dans une fonction, cachedvous pouvez organiser la mise en cache des résultats d'un appel à la fonction qui l'entoure en utilisant un objet normal ( Object) ou en utilisant un objet qui est une structure de donnéesMap...



Voici à quoi pourrait ressembler le code de fonction cached:



function cached(fn){
  //     ,      fn.
  const cache = Object.create(null);

  //   fn,    .
  return function cachedFn (str) {

    //       -   fn
    if ( !cache[str] ) {
        let result = fn(str);

        // ,   fn,   
        cache[str] = result;
    }

    return cache[str]
  }
}


Voici les résultats de l'expérimentation de cette fonctionnalité dans la console du navigateur.





Expérimenter une fonction dont les résultats sont mis en cache



Fonctions paresseuses



Dans les corps de fonction, il existe généralement des instructions pour vérifier certaines conditions. Parfois, les conditions qui leur correspondent ne doivent être vérifiées qu'une seule fois. Il est inutile de les vérifier à chaque fois que la fonction est appelée.



Dans de telles circonstances, vous pouvez améliorer les performances de la fonction en "supprimant" ces instructions après leur première exécution. En conséquence, il s'avère que la fonction, avec ses appels ultérieurs, n'aura pas à effectuer de vérifications, ce qui ne sera plus nécessaire. Ce sera la fonction "paresseuse".



Supposons que nous devions écrire une fonction fooqui renvoie toujours l'objet Datecréé la première fois que cette fonction est appelée. Veuillez noter que nous avons besoin d'un objet qui a été créé exactement lorsque la fonction a été appelée pour la première fois.



Son code pourrait ressembler à ceci:



let fooFirstExecutedDate = null;
function foo() {
    if ( fooFirstExecutedDate != null) {
      return fooFirstExecutedDate;
    } else {
      fooFirstExecutedDate = new Date()
      return fooFirstExecutedDate;
    }
}


Chaque fois que cette fonction est appelée, la condition doit être vérifiée. Si cette condition est très difficile, les appels à une telle fonction entraîneront une baisse des performances du programme. C'est là que nous pouvons utiliser la technique de création de fonctions "paresseuses" pour optimiser le code.



À savoir, nous pouvons réécrire la fonction comme suit:



var foo = function() {
    var t = new Date();
    foo = function() {
        return t;
    };
    return foo();
}


Après le premier appel à la fonction, nous remplaçons la fonction d'origine par la nouvelle. Cette nouvelle fonction renvoie la valeur treprésentée par l'objet Datecréé la première fois que la fonction a été appelée. Par conséquent, aucune condition ne doit être vérifiée lors de l'appel d'une telle fonction. Cette approche peut améliorer les performances de votre code.



C'était un exemple conditionnel très simple. Regardons maintenant quelque chose de plus proche de la réalité.



Lorsque vous attachez des gestionnaires d'événements à des éléments DOM, vous devez effectuer des vérifications pour vous assurer que la solution est compatible avec les navigateurs modernes et avec IE:



function addEvent (type, el, fn) {
    if (window.addEventListener) {
        el.addEventListener(type, fn, false);
    }
    else if(window.attachEvent){
        el.attachEvent('on' + type, fn);
    }
}


Il s'avère qu'à chaque fois que nous appelons une fonction addEvent, une condition y est vérifiée, ce qui suffit à ne vérifier qu'une seule fois, lors de son premier appel. Rendons cette fonction "paresseuse":



function addEvent (type, el, fn) {
  if (window.addEventListener) {
      addEvent = function (type, el, fn) {
          el.addEventListener(type, fn, false);
      }
  } else if(window.attachEvent){
      addEvent = function (type, el, fn) {
          el.attachEvent('on' + type, fn);
      }
  }
  addEvent(type, el, fn)
}


En conséquence, nous pouvons dire que si une certaine condition est vérifiée dans une fonction, qui ne doit être effectuée qu'une seule fois, alors en appliquant la technique de création de fonctions "paresseuses", vous pouvez optimiser le code. À savoir, l'optimisation consiste dans le fait qu'après le premier contrôle de la condition, la fonction d'origine est remplacée par une nouvelle, dans laquelle il n'y a plus de contrôles de conditions.



Fonctions de curry



Le curry est une telle transformation d'une fonction, après application de laquelle une fonction qui devait auparavant être appelée en lui passant plusieurs arguments à la fois se transforme en une fonction qui peut être appelée en passant les arguments requis un à la fois.



En d'autres termes, nous parlons du fait qu'une fonction curry, qui nécessite plusieurs arguments pour fonctionner correctement, est capable d'accepter le premier d'entre eux et de renvoyer une fonction capable de prendre un second argument. Cette deuxième fonction, à son tour, retourne une nouvelle fonction qui prend un troisième argument et renvoie une nouvelle fonction. Cela continuera jusqu'à ce que le nombre d'arguments requis soit transmis à la fonction.



À quoi cela sert-il?



  • Le curry permet d'éviter les situations où une fonction doit être appelée en passant le même argument encore et encore.
  • Cette technique permet de créer des fonctions d'ordre supérieur. Il est extrêmement utile pour gérer les événements.
  • Grâce au currying, vous pouvez organiser la préparation préliminaire des fonctions pour effectuer certaines actions, puis réutiliser facilement ces fonctions dans votre code.


Considérez une fonction simple qui ajoute les nombres qui lui sont passés. Appelons ça add. Il prend trois opérandes comme arguments et renvoie leur somme:



function add(a,b,c){
 return a + b + c;
}


Une telle fonction peut être appelée en lui passant moins d'arguments qu'il n'en a besoin (bien que cela conduise au fait qu'elle renvoie quelque chose de complètement différent de ce que l'on attend d'elle). Il peut également être appelé avec plus d'arguments que celui prévu lors de sa création. Dans une telle situation, les arguments "inutiles" seront simplement ignorés. Expérimenter une fonction similaire pourrait ressembler à ceci:



add(1,2,3) --> 6 
add(1,2) --> NaN
add(1,2,3,4) --> 6 //  .


Comment curry une telle fonction?



Voici le code de la fonction curryqui vise à curry d'autres fonctions:



function curry(fn) {
    if (fn.length <= 1) return fn;
    const generator = (...args) => {
        if (fn.length === args.length) {

            return fn(...args)
        } else {
            return (...args2) => {

                return generator(...args, ...args2)
            }
        }
    }
    return generator
}


Voici les résultats de l'expérimentation de cette fonctionnalité dans la console du navigateur.





Expérimenter le curry dans la console du navigateur



Composition des fonctions



Supposons que vous ayez besoin d'écrire une fonction qui, en prenant comme entrée, par exemple, une chaîne bitfish, renvoie une chaîne HELLO, BITFISH.



Comme vous pouvez le voir, cette fonction a deux objectifs:



  • Concaténation de chaînes.
  • Conversion des caractères de la chaîne résultante en majuscules.


Voici à quoi pourrait ressembler le code d'une telle fonction:



let toUpperCase = function(x) { return x.toUpperCase(); };
let hello = function(x) { return 'HELLO, ' + x; };
let greet = function(x){
    return hello(toUpperCase(x));
};


Expérimentons avec.





Test d'une fonction dans la console du navigateur



Cette tâche comprend deux sous-tâches organisées en fonctions distinctes. En conséquence, le code de fonctiongreetest assez simple. S'il était nécessaire d'effectuer plus d'opérations sur les chaînes, alors la fonctiongreetcontiendrait une construction commefn3(fn2(fn1(fn0(x)))).



Simplifions la solution du problème et écrivons une fonction qui compose d'autres fonctions. Appelons çacompose. Voici son code:



let compose = function(f,g) {
    return function(x) {
        return f(g(x));
    };
};


Maintenant, la fonction greetpeut être créée en utilisant la fonction compose:



let greet = compose(hello, toUpperCase);
greet('kevin');


Utiliser une fonction composepour créer une nouvelle fonction basée sur deux fonctions existantes implique de créer une fonction qui appelle ces fonctions de gauche à droite. En conséquence, nous obtenons un code compact et facile à lire.



Maintenant, notre fonction composene prend que deux paramètres. Et nous aimerions qu'il puisse accepter n'importe quel nombre de paramètres.



Une fonction similaire, capable d'accepter n'importe quel nombre de paramètres, est disponible dans la célèbre bibliothèque de soulignements open source .



function compose() {
    var args = arguments;
    var start = args.length - 1;
    return function() {
        var i = start;
        var result = args[start].apply(this, arguments);
        while (i--) result = args[i].call(this, result);
        return result;
    };
};


En utilisant la composition de fonctions, vous pouvez rendre les relations logiques entre les fonctions plus compréhensibles, améliorer la lisibilité de votre code et jeter les bases de futures extensions et refactorisations.



Utilisez-vous une manière particulière de travailler avec des fonctions dans vos projets JavaScript?










All Articles