ÉLOQUENT JAVASCRIPT

3 e Édition


précédentsommaire

IV. Les fonctions

Les fonctions sont le pain et le beurre de la programmation JavaScript. Le concept d'envelopper une partie du programme dans une valeur a de nombreuses utilisations. Cela nous permet de structurer des programmes plus importants, de réduire les répétitions, d'associer des noms à des sous-programmes et d'isoler ces sous-programmes les uns des autres.

L'application la plus évidente des fonctions est la définition d'un nouveau vocabulaire. Créer de nouveaux mots en prose est généralement un mauvais style. Mais en programmation, c'est indispensable.

Les anglophones adultes typiques ont environ 20 000 mots dans leur vocabulaire. Peu de langages de programmation intègrent 20 000 commandes. Et le vocabulaire disponible tend à être défini plus précisément, et donc avec moins de souplesse, que dans le langage humain. Par conséquent, nous devons généralement introduire de nouveaux concepts pour éviter de nous répéter trop souvent.

IV-A. Définir une fonction

Une définition de fonction est une variable dont la valeur est une fonction.

Par exemple, ce code définit une fonction qui retourne le carré d'un nombre x  donné en paramètre :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
const square = function (x) {
  return x * x;
};

console.log(square(12));
// → 144

La déclaration d'une fonction commence par le mot-clef function (il existe une autre façon de les déclarer, que nous verrons plus tard). Les fonctions ont un ensemble de paramètres (dans ce cas, uniquement x) et un corps, qui contient les instructions à exécuter lors de l'appel de celle-ci. Le corps d'une fonction doit toujours être placé entre accolades, même s'il ne comporte qu'une seule instruction.

Une fonction peut avoir plusieurs paramètres ou aucun. Dans l'exemple suivant, makeNoise ne prend pas de paramètre :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
const makeNoise = function () {
  console.log("Pling!");
};

makeNoise ();
// → Pling!

Ici, power en prend deux :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
const power = function(base, exponent) {
  let result = 1;
  for (let count = 0; count < exponent; count++) {
    result *= base;
  }
  return result;
};

console.log(power(2, 10));
// → 1024

Certaines fonctions telles que power et square retournent une valeur, d'autres non, comme makeNoise. Une instruction return détermine la valeur renvoyée par la fonction. Lorsque l'interpréteur rencontre une telle instruction, il sort immédiatement de la fonction en cours et renvoie la valeur au code à l’origine de l’appel. L'utilisation de l’instruction return sans valeur retournera undefined Les fonctions qui ne possèdent pas d'instruction de , return telles que makeNoise, renvoient de la même manière undefined.

Les paramètres d'une fonction se comportent comme des variables, mais leur valeur initiale est donnée par l'appelant de la fonction et non par le code de la fonction elle-même.

IV-B. Fixations et portées

Chaque variable a une portée, qui est la partie du programme dans laquelle la variable est visible. Pour les variables définies en dehors de toute fonction ou bloc, la portée est le programme complet - vous pouvez vous référer à ces variables où vous voulez. Celles-ci sont appelées globales.

Mais les variables créées pour les paramètres de fonction ou déclarées à l'intérieur d'une fonction ne peuvent être référencées que dans celle-ci, elles sont donc locales. Chaque fois que la fonction est appelée, de nouvelles instances de ces variables sont créées. Ceci fournit une certaine isolation entre les fonctions - chaque appel de fonction agit dans son propre petit monde (son environnement local) et peut souvent être interprété sans trop savoir ce qui se passe dans l'environnement global.

Les variables déclarées avec let et const sont en fait locales au bloc dans lequel elles sont déclarées. Par conséquent, si vous créez l'un de ceux-ci dans une boucle, le code avant et après la boucle ne peut pas les utiliser. Les variables anciennes, créées avec le mot-clef , sont var lisibles dans toute la fonction dans laquelle elles apparaissent - ou dans la portée globale, si elles ne figurent pas dans une fonction.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
let x = 10;
if (true) {
  const y = 20;
  var z = 30;
  console.log (x + y + z);
  // → 60
}
// y n'est pas visible ici
console.log (x + z);
// → 40

Chaque portée peut « regarder » dans la portée qui l'entoure, donc x est visible à l'intérieur du bloc, dans cet exemple. Il existe plusieurs variables qui ont le même nom - dans ce cas, le code ne peut voir que la plus interne. Par exemple, lorsque le code dans la fonction half fait référence à n, il voit son propre n, pas le n global.

IV-C. Portée imbriquée

JavaScript distingue seulement les variables globales et locales. Des blocs et des fonctions peuvent être créés à l'intérieur d'autres blocs et fonctions, produisant plusieurs degrés de localité.

Par exemple, cette fonction - qui produit les ingrédients nécessaires pour faire un lot de hummus - déclare une autre fonction à l'intérieur :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
const hummus = function(factor) {
  const ingredient = function(amount, unit, name) {
    let ingredientAmount = amount * factor;
    if (ingredientAmount > 1) {
      unit += "s";
    }
    console.log(`${ingredientAmount} ${unit} ${name}`);
  };
  ingredient(1, "can", "chickpeas");
  ingredient(0.25, "cup", "tahini");
  ingredient(0.25, "cup", "lemon juice");
  ingredient(1, "clove", "garlic");
  ingredient(2, "tablespoon", "olive oil");
  ingredient(0.5, "teaspoon", "cumin");
};

Le code dans la fonction ingredient peut voir la variable factor de la fonction hummus. Mais ses variables locales, telles que unit ou ingredientAmount, ne sont pas visibles dans la fonction hummus.

L'ensemble des variables visibles à l'intérieur d'un bloc est déterminé par la place de ce bloc dans le texte du programme. Chaque portée locale peut également voir toutes les étendues locales qui la contiennent et toutes les portées peuvent voir la portée globale. Cette approche de la visibilité de variable est appelée portée lexicale.

IV-D. Les fonctions en tant que valeur

Le nom d'une fonction, déclarée en tant que variable, permet sa réutilisation. Une telle variable est définie une fois et n'est jamais modifiée. Cela facilite la confusion entre la fonction et son nom.

Il est possible de stocker une valeur de fonction dans une nouvelle variable, de la passer en argument à une fonction, etc. De même, une variable qui contient une fonction est toujours une variable régulière et peut, si elle n'est pas constante, se voir attribuer une nouvelle valeur, comme ceci :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
let launchMissiles = function() {
  missileSystem.launch("now");
};
if (safeMode) {
  launchMissiles = function() {/* do nothing */};
}

Au chapitre 5, nous aborderons l'utilisation des variables de fonction dans d'autres fonctions.

IV-E. Autres déclarations

Il existe un moyen légèrement plus court de créer une variable de fonction. Lorsque le mot-clef function est utilisé au début d'une instruction, il fonctionne différemment.

 
Sélectionnez
1.
2.
3.
function square(x) {
  return x * x;
}

Ceci est une déclaration de fonction. L'instruction définit la variable square et la fait pointer vers la fonction donnée. Il est légèrement plus facile à écrire et ne nécessite pas de point-virgule après la fonction.

Il y a une subtilité avec cette forme de définition de fonction.

 
Sélectionnez
1.
2.
3.
4.
5.
console.log("The future says:", future());

function future() {
  return "You'll never have flying cars";
}

Le code précédent fonctionne, même si la fonction est définie sous le code qui l'utilise. Les déclarations de fonction ne font pas partie du flux d'interprétation de haut en bas. Elles sont conceptuellement déplacées au sommet de l'exécution et peuvent être utilisées par tous les codes de cette portée. Ce code est parfois utile, car il permet de déclarer les fonctions là où elles sont le plus utilisées.

IV-F. Fonctions fléchées

Il y a une troisième forme de notation pour les fonctions, forme très différente des autres. Au lieu du mot-clef function, cette forme utilise une flèche (=>) composée d'un signe égal et d'un caractère supérieur à (à ne pas confondre avec l'opérateur supérieur ou égal à, qui est écrit >=).

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
const power = (base, exponent) => {
  let result = 1;
  for (let count = 0; count < exponent; count++) {
    result *= base;
  }
  return result;
};

La flèche vient après la liste des paramètres, elle est suivie par le corps de la fonction. Il exprime quelque chose comme « cette entrée (les paramètres) produit ce résultat (le corps) ».

Lorsqu'il n'y a qu'un seul nom de paramètre, vous pouvez omettre les parenthèses autour de la liste de paramètres. Si le corps est une expression unique, plutôt qu'un bloc entre accolades, cette expression sera renvoyée par la fonction.

Ces deux définitions de square font la même chose :

 
Sélectionnez
1.
2.
const square1 = (x) => { return x * x; };
const square2 = x => x * x;

Lorsqu'une fonction de flèche n'a aucun paramètre, sa liste de paramètres est simplement un ensemble vide de parenthèses.

 
Sélectionnez
1.
2.
3.
const horn = () => {
  console.log("Toot");
};

Les fonctions fléchées et le mot-clef function peuvent être utilisés ensemble. Mis à part un détail mineur, dont nous parlerons au chapitre 6, ils ont le même comportement. Les fonctions fléchées ont été ajoutées en 2015, principalement pour permettre d'écrire des expressions de petite taille de manière moins verbeuse. Nous les utiliserons beaucoup au chapitre 5.

IV-G. La pile d'exécution

La manière dont l'interpréteur passe par les fonctions est appelée la pile d'exécution.

Regardons cela de plus près. Voici un programme simple qui appelle une fonction :

 
Sélectionnez
1.
2.
3.
4.
5.
function greet(who) {
  console.log("Hello " + who);
}
greet("Harry");
console.log("Bye");

Un passage à travers ce programme se passe comme ceci : l'appel à greet transfère l'exécution au début de cette fonction (ligne 2). La fonction appelle console.log, qui récupère l'exécution, effectue son travail, puis « retourne » à la ligne 2. Là, elle atteint la fin de la fonction de greet, donc elle retourne à l'endroit qui l'a appelée, c’est-à-dire la ligne 4. L'appel au nouveau console.log s'exécute. Après cela, le programme arrive à son terme.

Nous pourrions montrer le flux de contrôle schématiquement comme ceci :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
not in function
   in greet
        in console.log
   in greet
not in function
   in console.log
not in function

Comme une fonction doit revenir à l'endroit où elle a été appelée lorsque les instructions du bloc se terminent (à l'accolade fermante ou à l'aide d'un return), l'ordinateur doit mémoriser le contexte dans lequel l'appel a eu lieu.

L'emplacement où l'ordinateur stocke ce contexte est la pile d'exécution. Chaque fois qu'une fonction est appelée, le contexte actuel est stocké sur cette pile. Lorsqu'une fonction se termine, elle supprime le contexte supérieur de la pile et utilise ce contexte pour continuer l'exécution.

Stocker cette pile nécessite de l'espace dans la mémoire de l'ordinateur. Lorsque la pile devient trop grande, l'exécution du programme échoue avec un message tel que « out of stack space » ou « too much recursion ». Le code suivant illustre cela en posant à l'ordinateur une question vraiment difficile qui provoque un va-et-vient infini entre deux fonctions. L'ordinateur ne peut conserver en mémoire une pile d'exécution infinie, ce code générera une erreur :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
function chicken() {
  return egg();
}
function egg() {
  return chicken();
}
console.log(chicken() + " came first.");
// → ??

IV-H. Arguments optionnels

Le code suivant est autorisé et s'exécute sans problème :

 
Sélectionnez
1.
2.
3.
function square(x) { return x * x; }
console.log(square(4, true, "hedgehog"));
// → 16

Nous avons défini un calcul de carré avec un seul paramètre. Pourtant, quand on l'appelle avec trois, le langage ne se plaint pas. Il ignore les arguments supplémentaires et calcule le carré du premier.

JavaScript est extrêmement large quant au nombre d'arguments que vous transmettez à une fonction. Si vous en passez trop, les arguments excédentaires sont ignorés. Si vous en passez trop peu, les paramètres manquants se voient attribuer la valeur undefined.

L'inconvénient de cela est qu'il est possible, voire probable, que vous transmettiez accidentellement un nombre d'arguments incorrect aux fonctions. Et le code aura un comportement inattendu.

L'avantage est que ce comportement peut être utilisé pour permettre à une fonction d'être appelée avec un nombre différent d'arguments. Par exemple, cette fonction minus essaye d'imiter l'opérateur « - » en agissant sur un ou deux arguments :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
function minus(a, b) {
  if (b === undefined) return -a;
  else return a - b;
}

console.log(minus(10));
// → -10
console.log(minus(10, 5));
// → 5

Si vous écrivez un opérateur = après un paramètre, suivi d'une expression, la valeur de cette expression remplacera l'argument lorsqu'il n'est pas donné.

Par exemple, cette version de power rend son deuxième argument facultatif. Si vous ne le fournissez pas, la valeur par défaut sera deux et la fonction l'utilisera ainsi.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
function power(base, exponent = 2) {
  let result = 1;
  for (let count = 0; count < exponent; count++) {
    result *= base;
  }
  return result;
}

console.log(power(4));
// → 16
console.log(power(2, 6));
// → 64

Dans le chapitre suivant, nous verrons comment un corps de fonction peut obtenir la liste complète des arguments qui lui ont été transmis. C’est utile, car cela permet à une fonction d'accepter un nombre quelconque d'arguments. Par exemple, console.log fait cela – il affiche toutes les valeurs qui lui sont données.

 
Sélectionnez
1.
2.
console.log("C", "O", 2);
// → C O 2

IV-I. Fermeture (Closure)

La possibilité de traiter les fonctions comme des valeurs, combinée au fait que les variables locales sont recréées chaque fois qu'une fonction est appelée, soulève une question intéressante. Qu'advient-il des variables locales lorsque l'appel de fonction, qui les a créées, est terminé ?

Le code suivant en montre un exemple. Il définit une fonction, wrapValue, qui crée une variable locale. Il retourne ensuite une fonction qui accède à et renvoie en retour cette variable locale.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
function wrapValue(n) {
  let local = n;
  return () => local;
}

let wrap1 = wrapValue(1);
let wrap2 = wrapValue(2);
console.log(wrap1());
// → 1
console.log(wrap2());
// → 2

Ce code fonctionne exactement comme demandé : les deux instances de la variable sont toujours accessibles. Cette situation est une bonne démonstration du fait que les variables locales sont recréées à chaque appel, et que les différents appels ne peuvent pas écraser les variables locales déclarée plus haut.

Cette fonctionnalité - être capable de référencer une instance spécifique d'une variable locale dans une portée englobante - s'appelle la fermeture (closure). Ce comportement vous permet non seulement de vous soucier de la durée de vie des variables, mais également de réutiliser les paramètres de fonction.

Avec un léger changement, nous pouvons transformer l'exemple précédent en un moyen de créer des fonctions qui se multiplient par une quantité arbitraire :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
function multiplier(factor) {
  return number => number * factor;
}

let twice = multiplier(2);
console.log(twice(5));
// → 10

Dans l'exemple wrapValue, déclarer la variable locale n'est pas nécessaire, car n est déjà une variable du scope de wrapValue.

Penser à des programmes comme celui-ci demande un peu de pratique. Un bon modèle mental est de penser que les valeurs fonctionnelles contiennent à la fois le code dans leur corps et l'environnement dans lequel elles sont créées. Lorsqu'il est appelé, le corps de la fonction voit l'environnement dans lequel il a été créé, pas l'environnement dans lequel il est appelé.

Dans l'exemple, multiplier est appelé et crée un environnement dans lequel son paramètre factor est lié à la valeur 2. La valeur de la fonction renvoyée, où est stockée twice, mémorise cet environnement. Donc, quand la fonction multiplierest appelée, elle multiplie son argument par 2. la valeur de twice est appelée instance de multiplier.

IV-J. Récursion

Il est possible qu'une fonction s'appelle elle-même, à condition qu'elle respecte la limite de la pile d'exécution. Une fonction qui s'appelle elle-même est appelée récursive. La récursivité permet l'écriture de certaines fonctions dans un style différent. Prenons, par exemple, cette mise en œuvre différente du power :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
function power(base, exponent) {
  if (exponent == 0) {
    return 1;
  } else {
    return base * power(base, exponent - 1);
  }
}

console.log(power(2, 3));
// → 8

Ceci est assez proche de ce que les mathématiciens appellent l'exponentiation et décrit sans doute le concept plus clairement que la variante utilisant une boucle. La fonction s'appelle plusieurs fois avec des exposants de plus en plus petits pour obtenir le résultat.

Mais cette implémentation pose un problème : dans les implémentations JavaScript typiques, elle est environ trois fois plus lente que la version avec boucle. Passer par une simple boucle est généralement moins coûteux que d'appeler plusieurs fois une fonction.

Le dilemme de la vitesse contre l'élégance est intéressant. On peut y voir une sorte de continuum entre la convivialité humaine et la convivialité mécanique. Presque n'importe quel programme peut être rendu plus rapide en le rendant plus grand et plus compliqué. Le programmeur doit décider d'un équilibre approprié.

Dans le cas de la fonction de power, la version inélégante (en boucle) est encore assez simple et facile à lire. Cela n'a pas beaucoup de sens de la remplacer par la version récursive. Cependant, souvent, un programme traite de concepts si complexes qu'il est utile de renoncer à une certaine efficacité pour rendre le programme plus lisible.

Par conséquent, commencez toujours par écrire quelque chose de correct et facile à comprendre. Si vous craignez que l’exécution soit trop lente (ce qui n'est généralement pas le cas, car la plus grande partie du code n'est tout simplement pas exécutée assez souvent pour prendre beaucoup de temps), vous pouvez effectuer une mesure ultérieure et l'améliorer si nécessaire.

La récursivité n'est pas toujours une mauvaise solution de remplacement de la boucle. Certains problèmes sont plus faciles à résoudre avec la récursivité qu'avec les boucles. Le plus souvent, il s'agit de problèmes qui nécessitent d'explorer ou de traiter plusieurs « branches », chacune d'elles pouvant se ramifier dans encore plus de branches.

Considérez ce casse-tête :

En commençant par le nombre 1 et soit en ajoutant 5, soit en multipliant par 3, un nombre infini de nombres peut être obtenu. Comment écririez-vous une fonction qui, à partir d’un nombre, tente de trouver une séquence d’additions et de multiplications de ce type ?

Par exemple, le nombre 13 peut être atteint en multipliant d'abord par 3 et en ajoutant ensuite deux fois 5, alors que le nombre 15 ne peut pas être atteint du tout.

Voici une solution récursive :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
function findSolution(target) {
  function find(current, history) {
    if (current == target) {
      return history;
    } else if (current > target) {
      return null;
    } else {
      return find(current + 5, `(${history} + 5)`) ||
             find(current * 3, `(${history} * 3)`);
    }
  }
  return find(1, "1");
}

console.log(findSolution(24));
// → (((1 * 3) + 5) * 3)

Notez que ce programme ne trouve pas nécessairement la séquence d'opérations la plus courte. Il est satisfait quand il trouve ne séquence quelconque.

Ce n'est pas grave si vous ne voyez pas immédiatement comment cela fonctionne.

Il s'agit d'un excellent exercice de pensée récursive. Voici le fonctionnement :

La fonction interne find effectue la récursion réelle. Elle prend deux arguments : le nombre actuel et une chaîne qui enregistre comment nous avons atteint ce nombre. Si elle trouve une solution, elle renvoie une chaîne indiquant comment accéder à la cible. Si aucune solution ne peut être trouvée à partir de ce numéro, elle renvoie null.

Pour ce faire, la fonction effectue l'une des trois actions suivantes :

Si le nombre actuel est le numéro cible, l'historique actuel est un moyen d'atteindre cette cible, il est donc retourné. Si le nombre actuel est supérieur à la cible, il est inutile d'explorer davantage cette branche, car l'ajout et la multiplication ne feraient que rendre le nombre plus grand, il renvoie donc null. Enfin, si nous sommes toujours en dessous du nombre cible, la fonction essaye les deux chemins possibles qui partent du nombre actuel en s'appelant deux fois, une fois pour l'ajout et une fois pour la multiplication. Si le premier appel renvoie quelque chose qui n'est pas null, il est renvoyé. Sinon, le deuxième appel est renvoyé, qu'il produise une chaîne (String) ou null.

Pour mieux comprendre comment cette fonction produit l'effet recherché, examinons tous les appels à find lors de la recherche d'une solution pour le numéro 13.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
find(1, "1")
  find(6, "(1 + 5)")
    find(11, "((1 + 5) + 5)")
      find(16, "(((1 + 5) + 5) + 5)")
        too big
      find(33, "(((1 + 5) + 5) * 3)")
        too big
    find(18, "((1 + 5) * 3)")
      too big
  find(3, "(1 * 3)")
    find(8, "((1 * 3) + 5)")
      find(13, "(((1 * 3) + 5) + 5)")
        found!

L'indentation indique la profondeur de la pile d'appels. La première fois que la fonction find est appelée, elle commence par s'appeler elle-même pour explorer la solution qui commence par (1 + 5). Cet appel permettra en outre d'explorer toutes les solutions continues qui donnent un nombre inférieur ou égal au nombre cible. Comme il n'en trouve pas qui corresponde, il renvoie null au premier appel. Là, l'opérateur || provoque l'appel à l'exploration (1 * 3). Cette recherche a plus de chance - son premier appel récursif, via un autre appel récursif, obtient le numéro cible. Cet appel le plus interne renvoie une chaîne et chacun des || dans les appels intermédiaires transmet cette chaîne, retournant finalement la solution.

IV-K. Fonctions réutilisables

Il y a deux manières plus ou moins naturelles d'introduire des fonctions dans les programmes.

La première est pour éviter d'écrire plusieurs fois un même code. Avoir plus de code signifie plus d'erreurs reproduites et moins de lisibilité. La fonction répond à cette problématique.

La deuxième façon est le besoin d'une fonctionnalité potentiellement réutilisable. Vous commencerez par nommer la fonction, puis vous écrirez son corps. La difficulté de trouver un nom pour une fonction est une bonne indication de la clarté du concept que vous essayez d'encapsuler. Passons en revue un exemple.

Nous voulons écrire un programme qui logue deux nombres : le nombre de vaches et de poulets dans une ferme, avec les mots « Cows » et « Chickens » précédés par leur nombre à 3 chiffres.

007 Cows
011 Chickens

Cela nécessite une fonction prenant deux arguments : le nombre de vaches et le nombre de poulets. Voici le code :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
function printFarmInventory(cows, chickens) {
  let cowString = String(cows);
  while (cowString.length < 3) {
    cowString = "0" + cowString;
  }
  console.log(`${cowString} Cows`);
  let chickenString = String(chickens);
  while (chickenString.length < 3) {
    chickenString = "0" + chickenString;
  }
  console.log(`${chickenString} Chickens`);
}
printFarmInventory(7, 11);

L'écriture de .length juxtaposé au nom d’une variable contenant une chaîne de caractères, nous donnera la longueur de cette chaîne. Ainsi, les boucles while ajoutent des zéros devant les chaînes de nombres jusqu'à ce qu'elles aient au moins trois caractères.

Mission accomplie ! Mais au moment d'envoyer le code, il s'avère que nous devons aussi compter les cochons.

Nous pouvons certainement. Voici une première tentative utilisant le copier-coller de l'appel :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
function printZeroPaddedWithLabel(number, label) {
  let numberString = String(number);
  while (numberString.length < 3) {
    numberString = "0" + numberString;
  }
  console.log(`${numberString} ${label}`);
}

function printFarmInventory(cows, chickens, pigs) {
  printZeroPaddedWithLabel(cows, "Cows");
  printZeroPaddedWithLabel(chickens, "Chickens");
  printZeroPaddedWithLabel(pigs, "Pigs");
}

printFarmInventory(7, 11, 3);

Ça marche ! Mais ce nom, printZeroPaddedWithLabel, est un peu bizarre. Il confond trois choses : l'impression, l'ajout des zéros et l'ajout d'une étiquette dans une seule fonction.

Au lieu de supprimer la partie répétée de notre programme, rendons-la générique :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
function zeroPad(number, width) {
  let string = String(number);
  while (string.length < width) {
    string = "0" + string;
  }
  return string;
}

function printFarmInventory(cows, chickens, pigs) {
  console.log(`${zeroPad(cows, 3)} Cows`);
  console.log(`${zeroPad(chickens, 3)} Chickens`);
  console.log(`${zeroPad(pigs, 3)} Pigs`);
}

printFarmInventory(7, 16, 3);

Une fonction avec un nom lisible, comme zeroPad, facilite la lecture du code. Et une telle fonction est utile dans plus de situations que ce programme précédent. Par exemple, vous pouvez l'utiliser pour imprimer des tableaux de nombres bien alignés.

Dans quelle mesure notre fonction doit-elle être intelligente et polyvalente ? Nous pourrions écrire n'importe quoi, d'une fonction terriblement simple, qui ne peut contenir que trois caractères sur un nombre, à un système complexe de formatage numérique qui gère les nombres fractionnaires, les nombres négatifs, l'alignement des points décimaux, le remplissage avec différents caractères, etc.

Ne pas ajouter d'intelligence à moins d'être absolument certain que vous en aurez besoin est un principe de la programmation. Il peut être tentant d'écrire des « frameworks » généraux pour chaque fonctionnalité que vous rencontrerez. Résistez à cette envie. Vous n'obtiendrez aucun travail réel - vous écrirez simplement du code que vous n'utiliserez jamais.

IV-L. Synthèse

Ce chapitre vous a appris à écrire vos propres fonctions. Le mot-clef , function utilisé comme expression, crée une fonction. Utilisé en tant que déclaration, il peut être utilisé pour déclarer une variable et lui attribuer une fonction. Les fonctions fléchées sont encore une autre façon de créer des fonctions.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
// Define f to hold a function value
const f = function(a) {
  console.log(a + 2);
};

// Declare g to be a function
function g(a, b) {
  return a * b * 3.5;
}

// A less verbose function value
let h = a => a % 3;

Un aspect essentiel de la compréhension des fonctions est la maîtrise des portées d'application. Chaque bloc crée une nouvelle portée. Les paramètres et variables déclarés dans une portée sont locaux à cette portée et non visibles de l'extérieur. Les variables déclarées avec var se comportent différemment : leur portée est globale.

Séparer les tâches effectuées par votre programme en différentes fonctions est utile. Vous n'aurez plus besoin de vous répéter, et les fonctions peuvent vous aider à organiser un programme en regroupant le code en plusieurs éléments.

IV-M. Exercices

IV-M-1. Le minimum

Le chapitre précédent a introduit la fonction standard Math.min qui renvoie son plus petit argument. Vous allez recoder cette fonction. Écrivez une fonction min qui prend deux arguments et retourne leur minimum.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
// Votre code ici.

console.log(min(0, 10));
// → 0
console.log(min(0, -10));
// → -10

Si vous avez des difficultés à placer les accolades et les parenthèses au bon endroit pour obtenir une définition de fonction valide, commencez par copier un des exemples de ce chapitre et modifiez-le.

Une fonction peut contenir plusieurs déclarations de retour.

IV-M-2. Récursion

Nous avons vu que % (l’opérateur modulo) peut être utilisé pour tester si un nombre est pair ou impair en utilisant % 2 pour savoir s'il est divisible par deux. Voici une autre façon de définir si un nombre entier positif est pair ou impair :

  • 0 est pair ;
  • 1 est impair ;
  • pour tout autre nombre N, sa régularité est la même que N - 2.

Définissez une fonction récursive isEven correspondant à cette description. La fonction devra accepter un seul paramètre (un nombre entier positif) et renvoyer un booléen.

Testez-la sur 50 et 75. Voyez comment elle se comporte sur -1.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
// Votre code ici.

console.log(isEven(50));
// → true
console.log(isEven(75));
// → false
console.log(isEven(-1));
// → ??

Votre fonction ressemblera probablement un peu à la fonction interne find dans l'exemple récursif findSolution de ce chapitre, avec une chaîne if/else if/else qui teste lequel des trois cas s'applique. Le dernier else, correspondant au troisième cas, fait l'appel récursif. Chacune des branches doit contenir une déclaration de retour ou, d'une autre manière, faire en sorte qu'une valeur spécifique soit retournée.

Lorsqu'on lui donne un nombre négatif, la fonction va boucler indéfiniment , en se passant un nombre de plus en plus négatif, s'éloignant ainsi de plus en plus du retour d'un résultat. Elle finira par manquer d'espace dans la pile et annulera.

IV-M-3. Comptage des majuscules

Vous pouvez obtenir le Nième caractère, ou lettre, d'une chaîne en écrivant "string"[N]. La valeur renvoyée sera une chaîne contenant un seul caractère (par exemple, ) "b". Le premier caractère se trouve à la position 0, donc, le dernier se trouve à . En d'autres termes, une string.length – 1 chaîne de deux caractères a une longueur de 2 et ses caractères ont les positions 0 et 1.

Écrivez une fonction countBs qui prend une chaîne comme seul argument et retourne le nombre de caractères majuscules «B» dans la chaîne.

Ensuite, écrivez une fonction appelée countChar qui se comporte comme des countBs, sauf qu'elle prend un second argument qui indique le caractère à compter (plutôt que de ne compter que les caractères majuscules « B »).Réécrivez countBs pour utiliser cette nouvelle fonction.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
// Votre code ici.

 console.log(countBs("BBC"));
 // → 2
 console.log(countChar("kakkerlak", "k"));
 // → 4

Votre fonction aura besoin d'une boucle qui examine chaque caractère de la chaîne. Elle peut exécuter un index de zéro à un en dessous de sa longueur (< string.length). Si le caractère à la position courante est le même que celui que la fonction recherche, elle ajoute 1 à une variable compteur. Une fois la boucle terminée, le compteur peut être retourné.

Prenez soin de rendre locales à la fonction toutes les liaisons utilisées dans cette fonction en déclarant ces liaisons correctement avec le mot-clef let ou const.


précédentsommaire

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2019 Marijn Haverbeke. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.