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