IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

JavaScript Éloquent

Une introduction moderne à la programmation
Image non disponible


précédentsommairesuivant

III. Fonctions

Un programme doit souvent exécuter la même tâche en différents endroits. Il est fastidieux de répéter à chaque fois les instructions nécessaires et c'est un facteur d'erreurs possibles. Il vaudrait mieux réunir ces instructions au même endroit et demander au programme d'y faire un détour chaque fois que c'est nécessaire. C'est pour ça qu'on a inventé les fonctions : ce sont des unités de code que le programme peut parcourir à volonté. Afficher une chaîne à l'écran nécessite un certain nombre de déclarations, mais si nous disposons d'une fonction print il nous suffit d'écrire print("Aleph") et le tour est joué.

Cependant, si on voit les fonctions simplement comme des boîtes de conserve de code on ne les considère pas à leur juste valeur. Si nécessaire, elles peuvent jouer les rôles de fonctions pures, d'algorithmes, de détours, d'abstractions, de moyens de décision, de modules, de prolongements, de structures de données et de beaucoup d'autres. Être capable d'utiliser efficacement des fonctions est une compétence nécessaire pour qui veut programmer sérieusement. Ce chapitre propose une introduction au sujet, le chapitre 6 aborde plus en profondeur les subtilités des fonctions.

Pour commencer, les fonctions pures sont ce que l'on appelait « fonction » en cours de mathématiques, que vous avez, je l'espère, suivi à un moment de votre vie. Prendre le cosinus ou la valeur absolue d'un nombre est une fonction pure à un argument. L'addition est une fonction pure à deux arguments.

Les propriétés qui définissent les fonctions pures sont celles qui retournent toujours la même valeur pour les mêmes arguments et n'ont jamais d'effet de bord. Elles prennent des arguments, retournent une valeur basée sur ces arguments, et ne perdent pas leur temps à faire autre chose.

En JavaScript, l'addition est un opérateur, mais elle peut être encapsulée dans une fonction comme ceci (et aussi inutile que cela puisse sembler, nous allons rencontrer des situations dans lesquelles ça sera vraiment utile).

 
Sélectionnez
function ajouter(a, b) {
  return a + b;
}
 
show(ajouter(2, 2));

ajouter est le nom de la fonction. a et b sont les noms des deux arguments.

Le mot-clé function est toujours utilisé lorsque l'on crée une fonction. Lorsqu'il est suivi d'un nom de variable, la fonction créée sera stockée sous ce nom. À la suite du nom, vient une liste de noms d'arguments, et enfin, après celle-ci le corps de la fonction. Contrairement à celles autour du corps d'une boucle while ou d'une instruction if, les accolades autour du corps d'une fonction sont obligatoires(8).

Le mot-clé return, suivi d'une expression, est utilisé pour déterminer la valeur qu'une fonction renvoie. Lorsque l'exécution arrive sur une instruction return, elle saute immédiatement hors de la fonction courante et transmet la valeur retournée au code qui a appelé la fonction. Une instruction return sans expression à la suite fait renvoyer undefined à la fonction.

Un corps peut évidemment avoir plus d'une instruction en son sein. Voici une fonction pour calculer des puissances (avec des exposants entiers positifs) :

 
Sélectionnez
function puissance(base, exposant) {
  var resultat = 1;
  for (var compteur = 0; compteur < exposant; compteur++)
    resultat *= base;
  return resultat;
}
 
show(puissance(2, 10));

Si vous avez résolu l'exercice 2.2, cette technique utilisée pour calculer une puissance devrait vous sembler familière.

Créer une variable (resultat) et la mettre à jour sont des effets de bord. Est-ce que je ne viens pas de dire que les fonctions pures n'ont pas d'effets de bord ?

Une variable créée à l'intérieur d'une fonction existe uniquement à l'intérieur de celle-ci. Heureusement, sinon le programmeur devrait trouver un nom différent pour chaque variable dont il a besoin dans un programme. Comme resultat existe uniquement à l'intérieur de puissance, le changement ne dure que jusqu'à ce que la fonction retourne quelque chose, et du point de vue du code qui l'appelle, il n'y a pas d'effet de bord.

Écrivez une fonction appelée absolu qui retourne la valeur absolue du nombre qui lui est donné en argument. La valeur absolue d'un nombre négatif est la version positive du même nombre, et la valeur absolue d'un nombre positif (ou zéro) est le nombre lui-même.

Ex. 3.1
Sélectionnez
function absolu(nombre) {
  if (nombre < 0)
    return -nombre;
  else
    return nombre;
}
 
show(absolu(-144));

Les fonctions pures ont deux propriétés très sympathiques. Il est facile de s'en souvenir et de les réutiliser.

Si une fonction est pure, un appel à celle-ci peut être considéré comme une chose indépendante. Si vous n'êtes pas sûr qu'elle fonctionne correctement, vous pouvez la tester en l'appelant directement depuis la console, ce qui est facile car elle ne dépend d'aucun contexte(9). Il est facile de faire ces tests automatiquement - d'écrire un programme qui teste une fonction spécifique. Les fonctions non pures peuvent renvoyer différentes valeurs basées sur toutes sortes de facteurs, et avoir des effets de bord qui pourraient être difficiles à tester et à prévoir.

Comme les fonctions pures sont autosuffisantes, elles ont tendance à être utiles et pertinentes dans un plus grand nombre de situations que les non pures. Prenez show, par exemple. L'utilité de cette fonction dépend de la présence d'un espace spécial à l'écran pour afficher sa sortie. Si cet espace n'existe pas, la fonction est inutile. Nous pouvons imaginer une fonction analogue, appelons-la format, qui prend une valeur en argument et renvoie une chaîne de caractères représentant cette valeur. Cette fonction est utile dans plus de situations que show.

Bien sûr, format ne résout pas le même problème que show, et aucune fonction pure ne sera capable de résoudre ce problème, parce que cela nécessite des effets de bord. Dans beaucoup de cas, les fonctions non pures sont exactement ce dont vous avez besoin. Dans d'autres cas, un problème peut être résolu avec une fonction pure, mais la variante non pure est beaucoup plus adaptée ou efficace.

Par conséquent, lorsque quelque chose peut facilement être exprimé par une fonction pure, écrivez-le ainsi. Mais ne vous sentez pas coupable d'avoir écrit des fonctions non pures.

Les fonctions avec effets de bord ne contiennent pas obligatoirement une instruction return. Si aucune instruction return n'est trouvée, la fonction renvoie undefined

 
Sélectionnez
function crier(message) {
  alert(message + " !!");
}
 
crier("Youpi");

Les noms des arguments d'une fonction sont disponibles comme variables au sein de celle-ci. Ils feront référence aux valeurs des arguments avec lesquels est appelée la fonction, et comme les variables normales créées à l'intérieur d'une fonction, ils n'existent pas à l'extérieur de celle-ci. En plus de l'environnement global, il y a aussi de plus petits environnements locaux créés par des appels de fonctions. Lorsque l'on cherche une variable à l'intérieur d'une fonction, l'environnement local est contrôlé en premier, et ensuite, seulement si la variable n'existe pas là, on la cherche dans l'environnement global. Cela permet à une variable à l'intérieur d'une fonction de masquer une variable globale du même nom.

 
Sélectionnez
function alertEstPrint(valeur) {
  var alert = print;
  alert(valeur);
}
 
alertEstPrint("Troglodytes");

Les variables dans cet environnement local sont visibles seulement pour le code à l'intérieur de la fonction. Si cette fonction appelle une autre fonction, la fonction nouvellement créée ne voit pas les variables à l'intérieur de la première fonction.

 
Sélectionnez
var variable = "globale";
 
function afficherVariable() {
  print("à l'intérieur de afficherVariable, la variable contient '" +
        variable + "'.");
}
 
function test() {
  var variable = "locale";
  print("à l'intérieur de test, la variable contient '" + variable + "'.");
  afficherVariable();
}
 
test();

Cependant, et c'est un phénomène subtil mais extrêmement utile, lorsqu'une fonction est définie à l'intérieur d'une autre fonction, son environnement local sera basé sur l'environnement local qui l'entoure plutôt que sur l'environnement global.

 
Sélectionnez
var variable = "globale";
function fonctionParente() {
  var variable = "locale";
  function fonctionFille() {
    print(variable);
  }
  fonctionFille();
}
fonctionParente();

Au final, la visibilité des variables à l'intérieur d'une fonction est déterminée par la place de cette fonction dans le texte du programme. Toutes les variables définies « au-dessus » de la définition d'une fonction sont visibles, ce qui signifie à la fois celles dans les corps des fonctions qui la renferment et celles globales pour tout le programme. Cette approche de la visibilité des variables est appelée portée lexicale.

Les gens qui ont l'expérience d'autres langages de programmation pourraient s'attendre à ce qu'un bloc de code (entre accolades) crée également un nouvel environnement local. Pas en JavaScript. Les fonctions sont les seules qui délimitent une nouvelle portée. Vous avez le droit d'utiliser des blocs libres comme ceci...

 
Sélectionnez
var quelqueChose = 1;
{
  var quelqueChose = 2;
  print("À l'intérieur : " + quelqueChose);
}
print("À l'extérieur : " + quelqueChose);

... mais le quelqueChose à l'intérieur du bloc fait référence à la même variable que celui à l'extérieur du bloc. En fait, bien que les blocs comme celui-ci soient permis, ils sont parfaitement inutiles. La plupart des gens admettent que c'est une erreur de conception des créateurs de JavaScript, et ECMAScript Harmony ajoutera certains moyens de définir des variables qui restent à l'intérieur des blocs (le mot-clé let).

Voici un cas qui pourrait vous surprendre :

 
Sélectionnez
var variable = "globale";
function fonctionParente() {
  var variable = "locale";
  function fonctionFille() {
    print(variable);
  }
  return fonctionFille;
}
 
var fille = fonctionParente();
fille();

fonctionParente renvoie ses fonctions internes et le code en bas de l'appel à cette fonction. Même si fonctionParente a fini de s'exécuter à ce moment-là, l'environnement local dans lequel variable a la valeur locale existe toujours, et fonctionFille continue de l'utiliser. Ce phénomène s'appelle une fermeture lexicale (ou closure en anglais).

La portée lexicale permet non seulement de rendre très facile et rapide à discerner dans quelle partie d'un programme, une variable sera disponible, mais aussi de « synthétiser » des fonctions. En utilisant certaines des variables venant d'une fonction l'englobant, une fonction interne peut être amenée à faire des choses différentes. Imaginez que nous ayons besoin de plusieurs fonctions différentes mais similaires, l'une d'entre elles ajoutant 2 à son argument, l'autre ajoutant 5 et ainsi de suite.

 
Sélectionnez
function creerFonctionAjouter(quantite) {
  function ajouter(nombre) {
    return nombre + quantite;
  }
  return ajouter;
}
 
var ajouterDeux = creerFonctionAjouter(2);
var ajouterCinq = creerFonctionAjouter(5);
show(ajouterDeux(1) + ajouterCinq(1));

Pour vous rendre compte de cela, vous ne devez pas considérer que les fonctions empaquettent seulement des calculs, mais aussi un environnement. Les fonctions de haut niveau exécutent simplement l'environnement de haut niveau, c'est assez évident. Mais une fonction définie à l'intérieur d'une autre fonction conserve l'accès à l'environnement existant dans cette fonction à l'instant où elle a été définie.

Par conséquent, la fonction ajouter de l'exemple au-dessus - qui est créée lorsque creerFonctionAjouter est appelée - capture un environnement dans lequel quantite a une certaine valeur. Il empaquette cet environnement avec le calcul return nombre + quantite à l'intérieur d'une valeur qui est alors retournée depuis la fonction extérieur.

Lorsque cette fonction renvoyée (ajouterDeux ou ajouterCinq) est appelée, un nouvel environnement - dans lequel la variable nombre a une valeur - est créé comme un sous-environnement de l'environnement capturé (dans lequel quantite a une valeur). Ces deux valeurs sont ajoutées, et le résultat est renvoyé.

Au-delà du fait que différentes fonctions peuvent contenir des variables de même nom sans qu'elles ne se mélangent, ces règles de portée permettent également aux fonctions de s'appeler elles-mêmes sans que ça ne pose de problèmes. Une fonction qui s'appelle elle-même est qualifiée de récursive. La récursion permet de donner certaines définitions intéressantes. Jetez un coup d'œil à cette implémentation de puissance :

 
Sélectionnez
function puissance(base, exposant) {
  if (exposant == 0)
    return 1;
  else
    return base * puissance(base, exposant - 1);
}

C'est très proche de ce que les mathématiciens définissent comme l'exponentiation et, à mes yeux, c'est du code bien plus propre que dans la version initiale. C'est pour ainsi dire une boucle, mais sans while, for, ni même un effet de bord visible en local. En s'appelant elle-même, la fonction produit le même effet.

Il reste toutefois un problème important : dans la plupart des navigateurs, cette deuxième version est à peu près dix fois plus lente que la première. En JavaScript, faire tourner une boucle est bien plus économique qu'appeler une fonction à de multiples reprises.

Le dilemme entre vitesse et élégance est intéressant. Il n'apparaît pas seulement quand on décide de faire ou non une récursion. Dans de nombreuses situations, une solution élégante, intuitive et souvent plus courte peut être remplacée par une solution plus sophistiquée mais plus rapide.

Dans le cas de la fonction puissance ci-dessus la version peu élégante est encore suffisamment simple et facile à lire. Cela n'aurait pas d'intérêt de la remplacer par une version récursive. Pourtant il arrive souvent que les concepts que traite un programme deviennent si complexes qu'il devient tentant de renoncer à un peu d'efficacité pour gagner en simplicité.

La règle de base, qui a été répétée par de nombreux programmeurs et que j'approuve de toutes mes forces, c'est de ne pas s'inquiéter de l'efficacité tant que le programme ne devient pas un peu trop lent. Lorsque c'est le cas, trouvez quelles parties ralentissent l'exécution et commencez à viser l'efficacité plutôt que l'élégance.

Bien entendu, la règle ci-dessus ne signifie pas qu'on devrait démarrer en ignorant complètement le critère de performance. Dans de nombreux cas, comme la fonction puissance, on ne gagne que très peu de simplicité avec l'approche « élégante ». Dans d'autres cas, un programmeur expérimenté peut voir tout de suite que la simplicité ne sera jamais assez rapide.

La raison pour laquelle j'en fais toute une histoire est que bizarrement beaucoup de programmeurs se concentrent fanatiquement sur l'efficacité, y compris dans les détails les plus insignifiants. Résultat, les programmes sont plus longs, plus compliqués et souvent moins corrects, ils prennent plus de temps à écrire que leur équivalent simple et ne s'exécutent plus vite que de façon marginale.

Mais revenons à nos récursions. Un concept étroitement lié à la récursion est une chose qu'on appelle la pile. Quand on appelle une fonction, on donne le contrôle au corps de cette fonction. Quand le corps est exécuté, le code qui a appelé la fonction reprend. Pendant que le corps est exécuté, l'ordinateur doit se souvenir du contexte à partir duquel on a appelé la fonction pour savoir où reprendre par la suite. L'endroit où ce contexte est stocké est appelé la pile.

Le fait qu'on l'appelle une « pile » vient du fait que, comme nous l'avons vu, un corps de fonction peut appeler à nouveau une fonction. À chaque fois qu'une fonction est appelée, un autre contexte doit être stocké. On peut se le représenter comme une pile de contextes. À chaque appel de fonction, le contexte courant est mis sur le haut de la pile. Quand une fonction se termine, le contexte du haut de la pile en est retiré pour être restauré.

Cette pile nécessite un espace de stockage dans la mémoire de l'ordinateur. Quand la pile prend trop d'ampleur, l'ordinateur abandonne l'exécution en cours avec un message du genre « plus d'espace disponible dans la pile » ou « trop de récursions ». Mieux vaut s'en souvenir quand on écrit des fonctions récursives.

 
Sélectionnez
function poule() {
  return oeuf();
}
function oeuf() {
  return poule();
}
print(poule() + " était là en premier.");

Non seulement cet exemple nous expose une manière très intéressante d'écrire un programme qui plante, mais il montre aussi qu'une fonction n'a pas à s'appeler elle-même directement pour être récursive. Si elle appelle une autre fonction qui (directement ou non) appelle à nouveau la première, elle est quand même récursive.

La récursion n'est pas toujours juste une option moins efficace qu'une boucle. Certains problèmes sont bien plus faciles à résoudre avec une récursion qu'avec des boucles. Il s'agit le plus souvent de problèmes qui exigent l'exploration de plusieurs « branches », chacune d'elles pouvant à son tour se subdiviser en autres branches.

Réfléchissez à cette énigme : en partant du nombre 1 et en lui ajoutant toujours 5 ou bien en le multipliant toujours par 3, on peut générer une quantité infinie de nouveaux nombres. Comment écririez-vous une fonction qui, étant donné un nombre, essaie de trouver une suite d'additions et de multiplications qui produit ce nombre ?

Par exemple le nombre 13 peut être obtenu en multipliant d'abord 1 par 3, puis en ajoutant deux fois 5. Par contre, on ne peut pas obtenir le nombre 15.

Voici la solution :

 
Sélectionnez
function trouverSequence(objectif) {
  function trouver(debut, historique) {
    if (debut == objectif)
      return historique;
    else if (debut > objectif)
      return null;
    else
      return trouver(debut + 5, "(" + historique + " + 5)") ||
             trouver(debut * 3, "(" + historique + " * 3)");
  }
  return trouver(1, "1");
}
 
print(trouverSequence(24));

Notez que le programme ne trouve pas forcément la plus courte suite d'opérations, il estime avoir rempli sa mission dès qu'il trouve une combinaison quelconque d'opérations.

La fonction interne trouver, en s'appelant elle-même de deux façons différentes, explore à la fois la possibilité d'ajouter 5 au nombre courant et celle de le multiplier par 3. Quand le nombre voulu est trouvé, elle renvoie la chaîne historique, qui est utilisée pour enregistrer tous les opérateurs mis en œuvre pour parvenir au résultat. Elle vérifie également si le nombre courant est plus grand que objectif qui est le nombre recherché, puisque si c'est le cas, nous devons interrompre l'exploration de cette branche car elle ne peut nous donner le nombre que nous voulons.

L'utilisation de l'opérateur || dans l'exemple peut être comprise comme « renvoyer la solution trouvée en ajoutant 5 à debut et, si cela échoue, renvoyer la solution trouvée en multipliant debut par 3 ». On peut aussi écrire d'une façon plus verbeuse de la façon suivante :

 
Sélectionnez
else {
  var trouve = trouver(debut + 5, "(" + historique + " + 5)");
  if (trouve == null)
    trouve = trouver(debut * 3, historique + " * 3");
  return trouve;
}

Même si les définitions de fonctions interviennent comme des instructions au milieu du reste du programme, elles ne font pas partie de la même chronologie.

 
Sélectionnez
print("Le futur dit : ", futur());
 
function futur() {
  return "Nous n'avons TOUJOURS pas de voitures volantes.";
}

Ce qui se passe c'est que l'ordinateur examine toutes les définitions de fonctions et les stocke dans les fonctions associées, avant de commencer à exécuter le reste du programme. Il en va de même avec les fonctions qui sont définies à l'intérieur d'autres fonctions. Quand la fonction externe est appelée, la première chose qui se passe est que toutes les fonctions internes sont ajoutées au nouvel environnement.

Il existe une autre façon de définir des valeurs de type fonction, ressemblant davantage à la façon dont les autres valeurs sont créées. Quand le mot-clé function est utilisé dans un endroit où une expression est attendue, il est considéré comme une expression qui produit une valeur de type fonction. Les fonctions créées de cette façon n'ont même pas besoin d'être nommées (bien qu'il soit autorisé de le faire).

 
Sélectionnez
var ajouter = function(a, b) {
  return a + b;
};
show(ajouter(5, 5));

Notez le point-virgule après la définition de ajouter. Les définitions normales de fonctions n'en ont pas besoin, mais cette instruction a la structure générale de var ajouter = 22; et donc nécessite un point-virgule.

Ce type de valeur est appelé fonction anonyme, parce que la fonction définie n'a alors pas de nom. Parfois il est inutile de donner un nom aux fonctions, comme dans l'exemple précédant de creerFonctionAjouter :

 
Sélectionnez
function creerFonctionAjouter(quantite) {
  return function (nombre) {
    return nombre + quantite;
  };
}

Puisque dans la première version de creerFonctionAjouter, la fonction ajouter n'a servi qu'une fois, le nom n'est pas nécessaire et nous pouvons directement retourner la valeur de la fonction.

Écrivez une fonction plusGrandQue, qui prend un nombre en argument et retourne une fonction qui représente un test. Quand cette nouvelle fonction est appelée avec un simple nombre comme argument, elle retourne un booléen : true si le nombre donné est plus grand que le nombre utilisé pour créer la fonction, et false sinon.

Ex. 3.2
Sélectionnez
function plusGrandQue(x) {
  return function(y) {
    return y > x;
  };
}
 
var plusGrandQueDix = plusGrandQue(10);
show(plusGrandQueDix(9));

Essayez cela :

 
Sélectionnez
alert("Salut", "Bonsoir", "Comment allez-vous ?", "Au revoir");

La fonction alert n'accepte officiellement qu'un argument. Cependant, quand vous l'appelez ainsi, l'ordinateur ne se plaint pas, il ignore juste les autres arguments.

 
Sélectionnez
show();

Vous pouvez même, apparemment, vous passer d'arguments. Quand un argument n'est pas transmis, sa valeur dans la fonction est undefined.

Dans le chapitre suivant, nous verrons un moyen pour que le corps de la fonction connaisse la liste exacte des arguments qui lui sont donnés. Cela peut être utile, par exemple, pour réaliser une fonction qui accepte n'importe quel nombre d'arguments : print se comporte comme cela.

 
Sélectionnez
print("R", 2, "D", 2);

Bien sûr, un inconvénient est qu'il est aussi possible de donner un nombre incorrect d'arguments aux fonctions qui doivent en recevoir un nombre fixe, comme alert, et de ne pas en être prévenu.


précédentsommairesuivant
Techniquement, cela ne devrait pas être nécessaire, mais je suppose que les concepteurs de JavaScript se sont dit que cela clarifierait les choses si le corps des fonctions était toujours entouré d'accolades.
Techniquement, une fonction pure ne peut utiliser la valeur d'aucune variable externe. Ces valeurs pourraient changer et cela pourrait faire renvoyer une valeur différente pour les mêmes arguments. En pratique, le programmeur peut considérer certaines variables comme « constantes » - elles ne sont pas censées changer - et considérer les fonctions qui utilisent uniquement des variables constantes comme des fonctions pures. Les variables qui contiennent une fonction sont souvent de bons exemples de variables constantes.

Licence Creative Commons
Le contenu de cet article est rédigé par Marijn Haverbeke et est mis à disposition selon les termes de la Licence Creative Commons Attribution 3.0 non transposé.
Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright © 2013 Developpez.com.