11 juillet 2023

L'objet Function, NFE

Comme nous le savons déjà, une fonction en JavaScript est une valeur.

Chaque valeur en JavaScript a un type. De quel type est une fonction ?

Pour JavaScript, les fonctions sont des objets.

Un bon moyen d’imaginer des fonctions est en tant que des “objets d’action” qu’on peut appeler. Nous pouvons non seulement les appeler, mais aussi les traiter comme des objets : ajouter/supprimer des propriĂ©tĂ©s, passer par rĂ©fĂ©rence, etc.

La propriĂ©tĂ© “name”

Les objets Fonction contiennent des propriétés utilisables.

Par exemple, le nom d’une fonction est accessible en tant que propriĂ©tĂ© “name” :

function sayHi() {
  alert("Hi");
}

alert(sayHi.name); // sayHi

Ce qui est drĂŽle, c’est que la logique d’attribution de noms est intelligente. Elle attribue Ă©galement le nom correct Ă  une fonction mĂȘme si elle est créée sans, puis immĂ©diatement attribuĂ© :

let sayHi = function() {
  alert("Hi");
};

alert(sayHi.name); // sayHi (il y a un nom !)

Cela fonctionne aussi si l’affectation est faite avec une valeur par dĂ©faut :

function f(sayHi = function() {}) {
  alert(sayHi.name); // sayHi (ça marche !)
}

f();

Dans la spĂ©cification, cette fonctionnalitĂ© est appelĂ©e “contextual name”. Si la fonction n’en fournit pas, elle est dĂ©terminĂ©e Ă  partir du contexte lors de l’affectation.

Les mĂ©thodes d’objet ont aussi des noms :

let user = {

  sayHi() {
    // ...
  },

  sayBye: function() {
    // ...
  }

}

alert(user.sayHi.name); // sayHi
alert(user.sayBye.name); // sayBye

Cependant c’est pas magique. Il y a des cas oĂč il n’y a aucun moyen de trouver le bon nom. Dans ce cas, la propriĂ©tĂ© name est vide, comme ci-dessous :

// fonction créée dans un tableau
let arr = [function() {}];

alert( arr[0].name ); // <chaĂźne de caractĂšres vide>
// le moteur n'a aucun moyen de définir le bon nom. Donc, il n'y en a pas

Par contre, en pratique la plupart des fonctions ont un nom.

La propriĂ©tĂ© “length”

Il existe une autre propriĂ©tĂ© native, “length”, qui renvoie le nombre de paramĂštres de la fonction, par exemple :

function f1(a) {}
function f2(a, b) {}
function many(a, b, ...more) {}

alert(f1.length); // 1
alert(f2.length); // 2
alert(many.length); // 2

Nous pouvons voir que les paramÚtres du reste ne sont pas comptés.

La propriĂ©tĂ© length est parfois utilisĂ©e pour la rĂ©flĂ©xion (introspection en anglais) dans des fonctions qui opĂšrent sur d’autres fonctions.

Par exemple, dans le code ci-dessous, la fonction ask accepte une question Ă  poser et un nombre arbitraire de fonctions handler (gestionnaires) Ă  appeler.

Une fois qu’un utilisateur a fourni sa rĂ©ponse, la fonction appelle les gestionnaires. Nous pouvons transmettre deux types de gestionnaires :

  • Une fonction sans argument, qui n’est appelĂ©e que lorsque l’utilisateur donne une rĂ©ponse positive.
  • Une fonction avec des arguments, appelĂ©e dans les deux cas et renvoyant une rĂ©ponse.

Pour appeler handler correctement, nous examinons la propriété handler.length.

L’idĂ©e est que nous avons une syntaxe de gestionnaire simple, sans argument, pour les cas positifs (variante la plus frĂ©quente), mais que nous pouvons Ă©galement prendre en charge les gestionnaires universels :

function ask(question, ...handlers) {
  let isYes = confirm(question);

  for(let handler of handlers) {
    if (handler.length == 0) {
      if (isYes) handler();
    } else {
      handler(isYes);
    }
  }

}

// pour une réponse positive, les deux gestionnaires sont appelés
// pour une réponse négative, seulement le second
ask("Question?", () => alert('You said yes'), result => alert(result));

Ceci est un cas particulier de ce qu’on appelle le polymorphism – le traitement des arguments diffĂ©remment selon leur type ou, dans notre cas, en fonction de la length. Cette approche est utilisĂ©e dans les bibliothĂšques JavaScript.

Propriétés personnalisées

Nous pouvons également ajouter nos propres propriétés.

Nous ajoutons ici la propriĂ©tĂ© counter pour suivre le nombre total d’appels :

function sayHi() {
  alert("Hi");

  // comptons combien de fois nous executons
  sayHi.counter++;
}
sayHi.counter = 0; // valeur initiale

sayHi(); // Hi
sayHi(); // Hi

alert( `Called ${sayHi.counter} times` ); // Appelée 2 fois
Une propriĂ©tĂ© n’est pas une variable

Une propriĂ©tĂ© affectĂ©e Ă  une fonction comme sayHi.counter = 0 ne dĂ©finit pas une variable locale counter Ă  l’intĂ©rieur de celle-ci. En d’autres termes, une propriĂ©tĂ© counter et une variable let counter sont deux choses indĂ©pendantes.

On peut traiter une fonction comme un objet, y stocker des propriĂ©tĂ©s, mais cela n’a aucun effet sur son exĂ©cution. Les variables ne sont pas des propriĂ©tĂ©s de fonction et inversement. Ce sont des mondes parallĂšles.

Les propriĂ©tĂ©s de fonction peuvent parfois remplacer les fermetures. Par exemple, nous pouvons réécrire l’exemple de fonction de compteur du chapitre Variable scope, closure pour utiliser une propriĂ©tĂ© de fonction :

function makeCounter() {
  // au lieu de :
  // let count = 0

  function counter() {
    return counter.count++;
  };

  counter.count = 0;

  return counter;
}

let counter = makeCounter();
alert( counter() ); // 0
alert( counter() ); // 1

Le count est maintenant stocké dans la fonction directement, pas dans son environnement lexical externe.

Est-ce meilleur ou pire que d’utiliser une fermeture ?

La principale diffĂ©rence est que si la valeur de count rĂ©side dans une variable externe, le code externe ne peut pas y accĂ©der. Seules les fonctions imbriquĂ©es peuvent le modifier. Et si c’est liĂ© Ă  une fonction, une telle chose est possible :

function makeCounter() {

  function counter() {
    return counter.count++;
  };

  counter.count = 0;

  return counter;
}

let counter = makeCounter();

counter.count = 10;
alert( counter() ); // 10

Le choix dépend donc de nos objectifs.

Fonction Expression Nommée (NFE)

Fonction Expression NommĂ©e, ou NFE (“Named Function Expression” en anglais), est un terme pour les fonctions expressions qui ont un nom.

Par exemple, prenons une fonction expression ordinaire :

let sayHi = function(who) {
  alert(`Hello, ${who}`);
};

Et ajoutons un nom Ă  cela :

let sayHi = function func(who) {
  alert(`Hello, ${who}`);
};

Avons-nous réalisé quelque chose ici ? Quel est le but de ce nom supplémentaire "func" ?

Notons d’abord que nous avons toujours une expression de fonction. L’ajout du nom "func" aprĂšs function n’en a pas fait une dĂ©claration de fonction, car il est toujours créé dans le cadre d’une expression d’affectation.

L’ajout d’un tel nom n’a Ă©galement rien cassĂ©.

La fonction est toujours disponible sous la forme sayHi() :

let sayHi = function func(who) {
  alert(`Hello, ${who}`);
};

sayHi("John"); // Hello, John

Il y a deux particularités à propos du nom func, voici les raisons :

  1. Il permet à la fonction de se référencer en interne.
  2. Il n’est pas visible en dehors de la fonction.

Par exemple, la fonction sayHi ci-dessous s’appelle à nouveau avec "Guest" si aucun who est fourni :

let sayHi = function func(who) {
  if (who) {
    alert(`Hello, ${who}`);
  } else {
    func("Guest"); // utilise func pour se rappeler
  }
};

sayHi(); // Hello, Guest

// Mais ceci ne marchera pas :
func(); // Error, func is not defined (pas visible à l'extérieur de la fonction)

Pourquoi utilisons-nous func ? Peut-ĂȘtre juste utiliser sayHi pour l’appel imbriquĂ© ?

En fait, dans la plupart des cas, nous pouvons :

let sayHi = function(who) {
  if (who) {
    alert(`Hello, ${who}`);
  } else {
    sayHi("Guest");
  }
};

Le problÚme avec ce code est que sayHi peut changer dans le code externe. Si la fonction est assignée à une autre variable, le code commencera à donner des erreurs :

let sayHi = function(who) {
  if (who) {
    alert(`Hello, ${who}`);
  } else {
    sayHi("Guest"); // Error: sayHi is not a function
  }
};

let welcome = sayHi;
sayHi = null;

welcome(); // Error, l'appel sayHi imbriqué ne fonctionne plus !

Cela se produit parce que la fonction tire sayHi de son environnement lexical externe. Il n’y a pas de sayHi local, donc la variable externe est utilisĂ©e. Et au moment de l’appel, ce sayHi extĂ©rieur est null.

Le nom optionnel que nous pouvons mettre dans l’expression de fonction est destinĂ© Ă  rĂ©soudre exactement ce type de problĂšmes.

Utilisons-le pour corriger notre code :

let sayHi = function func(who) {
  if (who) {
    alert(`Hello, ${who}`);
  } else {
    func("Guest"); // Maintenant tout va bien
  }
};

let welcome = sayHi;
sayHi = null;

welcome(); // Hello, Guest (l'appel imbriqué fonctionne)

Maintenant cela fonctionne, car le nom 'func' est local Ă  la fonction. Il n’est pas pris de l’extĂ©rieur (et non visible lĂ -bas). La spĂ©cification garantit qu’elle fera toujours rĂ©fĂ©rence Ă  la fonction actuelle.

Le code externe a toujours sa variable sayHi ou welcome. Et func est un “nom de fonction interne”, la façon dont la fonction peut s’appeler de maniùre fiable.

Il n’y a rien de tel pour la dĂ©claration de fonction

La fonctionnalitĂ© “nom interne” dĂ©crite ici n’est disponible que pour les expressions de fonction, pas pour les dĂ©clarations de fonction. Pour les dĂ©clarations de fonctions, il n’y a aucune possibilitĂ© de syntaxe d’ajouter un nom “interne” supplĂ©mentaire.

Parfois, lorsque nous avons besoin d’un nom interne fiable, c’est la raison pour laquelle nous réécrivons une dĂ©claration de fonction en tant qu’expression de fonction nommĂ©e.

Résumé

Les fonctions sont des objets.

Ici nous avons couvert leurs propriétés :

  • name – le nom de la fonction. Habituellement tirĂ© de la dĂ©finition de la fonction, mais s’il n’en existe pas, JavaScript essaie de le deviner Ă  partir du contexte (par exemple, une affectation).
  • length – le nombre d’arguments dans la dĂ©finition de la fonction. Les paramĂštres du reste ne sont pas comptĂ©s.

Si la fonction est dĂ©clarĂ©e en tant qu’expression de fonction (et non dans le flux du code principal) et qu’elle porte un nom, elle est appelĂ©e expression de fonction nommĂ©e. Le nom peut ĂȘtre utilisĂ© Ă  l’intĂ©rieur pour se rĂ©fĂ©rencer, pour des appels rĂ©cursifs ou autres.

Les fonctions peuvent également comporter des propriétés supplémentaires. De nombreuses bibliothÚques JavaScript bien connues font un grand usage de cette fonctionnalité.

Elles crĂ©ent une fonction “principale” et y attachent de nombreuses autres fonctions “d’assistance”. Par exemple, la bibliothĂšque jQuery crĂ©e une fonction nommĂ©e $. La bibliothĂšque lodash crĂ©e une fonction _ et ajoute ensuite _.clone, _.keyBy et d’autres propriĂ©tĂ©s (voir la doc lorsque vous souhaitez en savoir plus Ă  leur sujet). En fait, elles le font pour rĂ©duire leur pollution de l’espace global, de sorte qu’une seule bibliothĂšque ne donne qu’une seule variable globale. Cela rĂ©duit la possibilitĂ© de conflits de noms.

Ainsi, une fonction peut faire un travail utile par elle-mĂȘme et aussi porter un tas d’autres fonctionnalitĂ©s dans les propriĂ©tĂ©s.

Exercices

importance: 5

Modifiez le code de makeCounter() afin que le compteur puisse également diminuer et définir le nombre:

  • counter() devrait retourner le nombre suivant (comme avant).
  • counter.set(value) devrait dĂ©finir le compteur Ă  value.
  • counter.decrease() devrait dĂ©crĂ©menter le compteur de 1.

Voir le code sandbox pour un exemple d’utilisation complet.

P.S. Vous pouvez utiliser une fermeture ou la propriété de fonction pour maintenir le nombre actuel. Ou écrivez les deux variantes.

Open a sandbox with tests.

La solution utilise count dans la variable locale, mais les mĂ©thodes d’addition sont Ă©crites directement dans le compteur. Ils partagent le mĂȘme environnement lexical extĂ©rieur et peuvent Ă©galement accĂ©der au count actuel.

function makeCounter() {
  let count = 0;

  function counter() {
    return count++;
  }

  counter.set = value => count = value;

  counter.decrease = () => count--;

  return counter;
}

Ouvrez la solution avec des tests dans une sandbox.

importance: 2

Écrivez la fonction sum qui fonctionnerait comme ceci:

sum(1)(2) == 3; // 1 + 2
sum(1)(2)(3) == 6; // 1 + 2 + 3
sum(5)(-1)(2) == 6
sum(6)(-1)(-2)(-3) == 0
sum(0)(1)(2)(3)(4)(5) == 15

P.S. Indice: vous devrez peut-ĂȘtre configurer une conversion d’objet Ă  primitive personnalisĂ© pour votre fonction.

Open a sandbox with tests.

  1. Pour que tout fonctionne * de toute façon *, le rĂ©sultat de sum doit ĂȘtre fonction.
  2. Cette fonction doit garder en mémoire la valeur actuelle entre les appels.
  3. Selon la tĂąche, la fonction doit devenir le numĂ©ro lorsqu’elle est utilisĂ©e dans ==. Les fonctions Ă©tant des objets, la conversion s’effectue comme dĂ©crit dans le chapitre Conversion d'objet en primitive, et nous pouvons fournir notre propre mĂ©thode qui renvoie le nombre.

Maintenant le code:

function sum(a) {

  let currentSum = a;

  function f(b) {
    currentSum += b;
    return f;
  }

  f.toString = function() {
    return currentSum;
  };

  return f;
}

alert( sum(1)(2) ); // 3
alert( sum(5)(-1)(2) ); // 6
alert( sum(6)(-1)(-2)(-3) ); // 0
alert( sum(0)(1)(2)(3)(4)(5) ); // 15

Veuillez noter que la fonction sum ne fonctionne rĂ©ellement qu’une fois. Il renvoie la fonction f.

Ensuite, Ă  chaque appel suivant, f ajoute son paramĂštre Ă  la somme currentSum, et se renvoie lui-mĂȘme.

Il n’y a pas de rĂ©cursion dans la derniĂšre ligne de f.

Voici à quoi ressemble la récursion:

function f(b) {
  currentSum += b;
  return f(); // <-- appel récursif
}

Et dans notre cas, nous renvoyons simplement la fonction, sans l’appeler:

function f(b) {
  currentSum += b;
  return f; // <-- ne s'appelle pas, se renvoie
}

Ce f sera utilisĂ© lors du prochain appel et se renvera lui-mĂȘme autant de fois que nĂ©cessaire. Ensuite, lorsqu’il est utilisĂ© sous forme de nombre ou de chaĂźne de caractĂšres, le toString renvoie le currentSum. Nous pourrions aussi utiliser Symbol.toPrimitive ou valueOf ici pour la conversion.

Ouvrez la solution avec des tests dans une sandbox.

Carte du tutoriel

Commentaires

lire ceci avant de commenter

  • Si vous avez des amĂ©liorations Ă  suggĂ©rer, merci de soumettre une issue GitHub ou une pull request au lieu de commenter.
  • Si vous ne comprenez pas quelque chose dans l'article, merci de prĂ©ciser.
  • Pour insĂ©rer quelques bouts de code, utilisez la balise <code>, pour plusieurs lignes – enveloppez-les avec la balise <pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepen
)