Pour les Jedis, JavaScript, épisode I : Au coeur des fonctions (partie 2)

Cet article fait partie de la série “Pour les Jedis, JavaScript” d’articles consacrés à JavaScript.
  1. Pour les Jedis, JavaScript, épisode I : Au coeur des Fonctions (1 et 2)
  2. Pour les Jedis, JavaScript, épisode II : L’attaque des Closures (1 et 2)
  3. Pour les Jedis, JavaScript, épisode III : La revanche des Prototypes (1 et 2)

Invocation en tant que fonction

Ce type d’invocation est sûrement le plus utilisé. En effet, si une fonction n’est pas invoquée en tant que méthode, constructeur ou via apply() ou call(), alors nous sommes en présence d’invocation d’une fonction.

Cette invocation se produit lorsque la fonction est invoquée en utilisant les parenthèses et lorsque cette fonction n’est pas une propriété d’un objet. Voici un exemple plus parlant :

Lorsqu’il est invoqué de cette manière, le contexte d’invocation de ces fonctions est window, le contexte globale en JavaScript.

Invocation en tant que méthode

Lorsqu’une fonction est une propriété d’un objet et que l’invocation se fait en appelant cette fonction, alors la fonction est invoquée en tant que méthode de cet objet. Pour illustrer cela, étudions cet exemple :

Invocation en tant que constructeur

Un constructeur est une simple fonction, il n’a vraiment rien de spécial. La seule différence réside dans la façon dont elle va être invoquée. Pour invoquer une fonction en tant que constructeur, nous le précédons par le mot-clé new.

A noter que ES2015 a introduit la notion de Class en JavaScript, ce qui est simplement un sucre syntaxique autour des fonctions en tant que constructeurs.

Invoquer une fonction en tant que constructeur est un aspect très intéressant de JavaScript, car lorsqu’un constructeur est invoqué, plusieurs choses se produisent :

  • un nouvel objet vide est créé, le fameux paramètre this;
  • cet objet est passé au constructeur. Il devient donc le contexte d’invocation ;

En l’absence d’une instruction return explicite dans le constructeur, cet objet est retourné par le constructeur. Prenons cet exemple :

Nous avons déclaré un constructeur Foo, celui-ci déclare une propriété bar, qui est une méthode retournant le contexte this (pour l’exemple).

Dans un premier temps, nous invoquons ce constructeur avec le mot-clé new et nous avons bien un objet de type Foo qui a été créé, et qui possède bien une méthode bar. Un petit test sur le contexte this dans foo, nous prouve que c’est bien une instance de Foo.

Dans l’exemple 2, nous avons essayé de démontrer qu’invoquer un constructeur en tant que fonction, sans le mot-clé new, ne donne pas du tout le résultat attendu. En effet, invoquer Foo en tant que fonction, exécute simplement le code de cette fonction, ce qui provoque la création de la propriété bar dans l’objet hôte — dans lequel Foo a été invoquée. Cet objet représenté par this dans Foo est bien évidemment window, puisque la fonction Foo a été déclarée dans le contexte global de JavaScript.

“Invoquer un constructeur en tant que fonction est risqué.”

Jusque là, nous avons vu que le type d’invocation d’une fonction agit directement sur le contexte d’invocation, représenté par le paramètre implicite this. Pour les méthodes c’est l’objet hôte ; pour les fonctions déclarées dans le contexte global c’est le window; et pour les constructeurs c’est l’instance du nouvel objet créé.

Maintenant, comment pouvons-nous faire si nous voulons forcer un contexte d’invocation particulier ? C’est bien grâce à apply() et call().

Invocation via apply() ou call()

JavaScript offre une méthode simple, pour invoquer une fonction et lui spécifier explicitement un contexte d’invocation. Nous pouvons réaliser cela grâce à deux méthodes proposées par toutes les fonctions : apply() et call().

Pourquoi voudrions-nous faire cela ? Prenons l’exemple d’une situation que nous rencontrons régulièrement en JavaScript : les actions — ou callbacks — des événements déjà abordés précédemment. Lorsque nous déclarons une fonction de callback sur un événement, celle-ci est invoquée lorsque l’événement en question est traité par le navigateur. Cette fonction de callback est invoquée avec le contexte de l’événement, c’est un comportement par défaut du navigateur.

Comme l’illustre le code ci-dessus, le contexte d’invocation de la fonction de callback — représenté par this— est celui de window ; la callback foo est une fonction déclarée dans le contexte globale. Elle a été invoquée en tant que fonction.

Voyons maintenant le même code mais cette fois-ci en utilisant la méthode call().

En invoquant une fonction — ou méthode — avec call(), nous passons un premier paramètre qui représente le contexte d’invocation. Dans notre exemple, ce contexte est l’élément DOM qui nous intéresse. En plus du contexte d’invocation, nous avons aussi passé une liste de paramètre à la fonction foo.

Avec apply(), c’est quasiment la même chose : nous passons également le contexte d’invocation en premier paramètre. Mais contrairement à call(), la méthode apply() prend en second paramètre un tableau représentant la liste des arguments.

Voici un autre exemple plus intéressant :

Dans cet exemple, nous avons déclaré une fonction every() qui permet d’itérer sur une liste d’éléments. Cette fonction prend en second paramètre une fonction de callback qui sera invoquée pour chaque élément parcouru, avec le contexte de la liste (nous aurions pu spécifier un autre contexte), et nous donne en paramètre l’index de l’élément courant.

Quelle méthode utiliser ?

Cela dépend de votre cas d’usage, et plus précisément cela dépend de comment vous devez gérer vos paramètres. Si vous avez un ensemble de variables que vous devez passer en tant que paramètres, call() serait idéale. Mais si vous avez déjà votre liste de variables sous forme de tableau (par exemple, arguments), apply() est plus appropriée. Prenons par exemple la méthode Math.max(n1, n2, n3, ...) de JavaScript : cette méthode calcule le maximum d’une liste de valeur qu’elle prend en paramètre :

Math.max(0, 1, 2, 3, 9); //=> 9 .

Supposons que vous voudriez calculer le maximum (ou le minimum) d’une suite de valeur dont vous ne connaissez pas la longueur :

Dans cet exemple, je viens de déclarer une fonction nommée max() qui calcule le maximum d’une suite de nombres passés en paramètre. S’il n’y a aucun paramètre, le résultat est zéro. Je me repose donc sur le paramètre implicites arguments pour réaliser cette vérification. Vous comprenez tout de suite qu’utiliser apply a tout son sens dans cet exemple. Veuillez noter également que j’ai passé Math en tant que contexte d’invocation à la méthode Math.max() : je n’étais pas obligé d’en spécifier un, mais c’est plus sûr comme ça !

Un mot sur bind()

En JavaScript, il existe une autre façon de contrôler le contexte d’invocation d’une fonction. Contrairement à call() et apply(), ce contrôle est fait au moment de la déclaration de la fonction, et non à l’invocation.

Ce contrôle est rendu possible grâce à la méthode bind(arg) permettant de transformer le contexte d’invocation d’une fonction. Jetons un œil à l’exemple suivant :

Nous avons déclaré un constructeur (ou Classe) Foo. Ce constructeur possède une méthode inc(), qui permet d’incrémenter un compteur counter. Ensuite, nous attachons un événement sur un élément DOM (disons, un bouton par exemple). A chaque clique nous souhaitons incrémenter le compteur de Foo.

Dans un premier temps (cas #1), nous attachons la méthode foo.inc en tant qu’action sur le click. Mais là, problème ! En effet, l’attribut counter n’est pas résolu ; tout simplement puisque le contexte d’invocation de foo.inc n’est pas l’instance foo mais element (rappelez vous l’exemple cité plus haut avec le apply()).

Pour résoudre ce problème, nous aurions pu passer par une fonction anonyme (cas #2) dans laquelle nous aurions invoqué la méthode foo.inc(). Mais pourquoi déclarer une fonction juste pour invoquer une autre ? Il y a plus simple, nous utilisons la méthode bind() en précisant foo comme contexte d’invocation (cas #3). Mais rappelez-vous que bind() n’invoque pas la fonction en question, elle modifie simplement son contexte d’invocation.

Voilà !

Dans ce premier épisode, nous avons étudié en détails un des aspects fascinant de JavaScript : JavaScript en tant que langage fonctionnel. J’espère qu’en comprenant comment les fonctions sont interprétées en interne par JavaScript, cela vous aidera à écrire un meilleur code JS, en passant d’un “simple” code qui “fait l’affaire” à un code digne d’un vrai Jedi que vous êtes dorénavant.

Voilà, nous avons atteint la fin de ce premier épisode ; rendez-vous pour le second épisode dans lequel nous allons tenter de percer le mystère des fermetures en JavaScript, connus sous le nom de Closures.

Que le JS soit avec vous…


Vous avez aimé cet article ? Ajoutez des claps et Suivez moi sur Medium et Twitter pour plus de contenu sur JavaScript et le Web 🎉