JavaScript, les bons éléments, façon 2020 : Fonctions fléchées, paramètres par défaut

L’ES6, en 2015, a été le symbole de beaucoup de choses, et les fonctions en ont fait partie.

Trois nouveautés majeures ont été dévoilées et intégrées dans la spécification cette année là : les paramètres variadiques, la possibilité d’avoir des valeurs par défaut et une troisième, les fonctions fléchées, qui sont un peu plus qu’un changement de syntaxe.

Les fonctions fléchées

Une nouvelle syntaxe pour écrire des fonctions anonymes

Les expressions de fonctions sont partout en JavaScript : vous pouvez écrire une fonction en plein milieu du code dès que vous en avez besoin.

Par exemple, supposons que vous vouliez dire au navigateur quoi faire quand l’utilisateur clique sur un bouton spécifique. Vous pouvez écrire :

// La méthode .addEventListener prend le type d'évènement ainsi qu'une fonction en paramètre
// Aucun problème en JavaScript, on peut l'écrire là où elle est attendue
document.getElementById('btn').addEventListener('click', function () {
  coucou();
  alert('hello');
});

Écrire ce genre de code semble naturel. Il peut donc paraître étrange de rappeler qu’avant que ce type de programmation ait été popularisé avec JavaScript, de nombreux langages n’avaient pas cette fonctionnalité.

Bien sûr, dès les années 50, le langage Lisp (qui a partiellement inspiré JavaScript) avait des expressions de fonctions, aussi appelées fonctions lambda. Toutefois de nombreux langages comme C++, Python, C#, et Java ont vécu des années sans elles.

Ce n’est plus le cas aujourd’hui. Ces quatre langages ont désormais des fonctions lambdas. Les langages les plus récents utilisent tous le concept des lambdas, et ceci est principalement dû à la popularité de JavaScript. Un grand merci aux premiers développeurs JavaScript qui ont écrit sans crainte des bibliothèques dépendant fortement des lambdas, ce qui a mené à l’adoption générale de celles-ci.

Malheureusement, de tous les languages évoqués ici, JavaScript est celui qui possède la syntaxe la moins concise pour les fonctions lambdas.

ES6 introduit ainsi une nouvelle syntaxe pour écrire des fonctions, appelée "fonction fléchée".

const values = [-2, -1, 0, 1, 2];

// Ancienne façon d'utiliser des expressions de fonction
const filtered = values.filter(function (element) {
  return element >= 0;
});

// Nouvelle manière ES6
const filtered = values.filter(element => element >= 0);

Lorsque vous avez besoin d’une fonction simple qui utilise un seul argument, vous pouvez utiliser la nouvelle syntaxe fléchée : nom_variable => expression. Il n’y a plus besoin d’écrire function, return, les parenthèses, les accolades, et le point-virgule.

Pour écrire une fonction avec plusieurs arguments (ou aucun argument), vous devrez ajouter des parenthèses autour de la liste d’arguments.

const sum = values.reduce(function (acc, val) {
  return acc + val;
}, 0);

// Avec les fonctions fléchées
const sum = values.reduce((acc, val) => acc + val, 0);

Que se passe-t-il avec des scénarios moins fonctionnels ? Les fonctions fléchées peuvent contenir un bloc d’instructions plutôt qu’une seule expression. Reprenons cet exemple :

// avant
document.getElementById('btn').addEventListener('click', function () {
  coucou();
  alert('hello');
});

Avec les fonctions fléchées, cela ressemble à ça :

document.getElementById('btn').addEventListener('click', () => {
  coucou();
  alert('hello');
});

Ici, ça n’est qu’une amélioration mineure. Notez que la fonction fléchée ne renvoie pas automatiquement une valeur dans ce cas. Pour cela, il faut utiliser l’instruction return.

Le cas "this" en JavaScript

Ah… this. En JavaScript, il a été décidé lors de la conception du langage qu’un mot clé, this pouvait référer à tout un tas de choses.

Initialement, c’est déroutant, car dans la quasi-intégralité des autres langages, this/$this/self ou équivalent n’a que pour utilité de référer à l’instance actuellement traitée dans une méthode de classe. Plus simplement, si on prend le code obj.method(), à l’intérieur de method, this vaudra obj à l’exécution.

En JavaScript, cela est vrai aussi (enfin, presque) !

Il faut distinguer plusieurs utilisations de this, car il dépend du contexte.

  • Cas 1 : L’objet global via this, le cas "non-contextualisé"

En non strict mode (c’est à dire hors d’un module et sans avoir précisé 'use strict' en début de fichier), utiliser this en dehors de tout contexte réfèra à l’objet global, généralement window.

Notez que si vous êtes en strict mode (dans un module donc, ou après un 'use strict'), alors utiliser this reverra undefined.

Qu’est-ce que signifie exactement "en dehors de tout contexte" ? C’est à dire qu’on se trouve dans un cas qui ne valide pas l’ensemble des cas présentés ensuite (c’est donc le cas par défaut).

Ce cas par défaut s’applique généralement :

  • À la racine d’un script, en dehors de toute fonction
  • Dans une fonction déclarée avec function, appelée sans l’opérateur new ou sans utilisation de méthode de fonction (.bind, .call et .apply)
console.log(this); // undefined en mode strict, window sinon

function hello() {
  console.log(this);
}

// Affiche également undefined en mode strict, window sinon
hello(); 
  • Cas 2 : L’appel de méthode via l’objet appelant

Comme dit précédement lors de l’introduction, si l’on dispose d’un objet obj avec une méthode method, alors écrire obj.method() permettra bien d’utiliser this comme prévu :

const obj = {
  data: [1, 3, 4],
  method: function (index) {
    return this.data[index];
  },
};

console.log(obj.method(1)); // Affiche 3 dans la console

Ici, le binding de this fonctionne car lors de l’appel à method, on l’a précédé de obj. : le moteur JavaScript sait ainsi qu’il doit associer this à obj.

Attention cependant ! Ici, on permet au moteur de binder this automatiquement, car il est précédé de obj.. Cependant, ce binding n’est pas écrit en dur dans la méthode, il est fait lors de l’appel. Si vous stockez la méthode dans une autre variable, ou dans un autre objet, alors le binding sera fait différemment.

// On stocke la méthode dans une variable
const the_method = obj.method;

// On l'appelle
the_method(1);
/// ...Aie ! 
// Uncaught TypeError: Cannot read property '1' of undefined

Ici, on a levé une TypeError, pourquoi ça ?

Le moteur JavaScript ne voit pas de obj. lors de l’appel, il ne déduit donc pas de binding automatique. Lors de l’appel à the_method, le moteur en déduit que c’est une fonction classique, comme si elle avait été déclarée en dehors de l’objet, le cas 1 s’applique donc : this vaut window/undefined.

Ici, l’erreur est levée lors de la recherche de '1' dans undefined, cela veut dire que le moteur a résolu la fonction comme ceci :

function (index) {
  this = window;
  data = this.data;
  index = 1;

  // window.data n'existe pas, donc {data} vaut undefined
  // Accéder à la propriété '1' de undefined lève une erreur ici
  return data[index];
}

Faites donc attention lorsque vous appelez des méthodes définies sur des objets… ou utilisez le cas 3 !

  • Cas 3 : Utiliser les méthodes définies dans Function.prototype

Le cas 2 est un cas d’école en JavaScript : Il est intrinsèquement lié à la manière dont le langage résoud l’ambiguité liée à this. Les concepteurs du langage ont ainsi pensé à des manières d’outrepasser le binding automatique.

Forcer le binding, via .call et .apply : On l’a déjà observé lors de cette série d’articles, on peut appeler une fonction de plusieurs façons en JavaScript (même si cela reste peu commun).

L’utilisation d’une de ces deux méthodes permet de fixer la valeur de this pour l’appel courant de la fonction. La différence entre .call et .apply se situe uniquement dans la syntaxe.

On va reprendre l’exemple de const the_method = obj.method; défini dans le cas 2.

the_method.call(obj, 2); // Renvoie 4 (le 3ème élément, index 2 de obj.data)
the_method.apply(obj, [2]); // Renvoie aussi 4

Comme vous voyez, pour les deux, vous précisez la valeur de this lors de l’appel comme premier argument.

Ensuite, pour .call, vous spécifiez les arguments de manière naturelle, séparés par des virgules. Pour .apply, passez l’intégralité des arguments dans un tableau, lui-même deuxième argument de .apply. C’est la seule différence.

Cette solution est bien pratique, mais un peu lourde si l’appel doit être répété plusieurs fois, voire pire, impossible si l’objet obj est hors de portée.

Définir une valeur statique de this, via .bind : La "méthode lourde" pour contrecarrer le problème soulevé plus haut est de carrément créer un alias de la fonction qui dispose d’une valeur fixe de this. En JavaScript, cela se passe avec .bind.

// On bind obj à this de manière statique sur l'alias {the_right_method}
const the_right_method = obj.method.bind(obj);

the_right_method(2); // 4 :D

La définition de this n’est faite que sur l’alias créé : Cela ne modifie pas method. De plus, cet alias a désormais une valeur de this totalement fixe : Rien ne peut la surcharger, ni .call, ni .apply, ni même un autre .bind.

  • Cas 4 : Le this d’un constructeur

Ce cas est également connu, c’est l’utilisation de this dans la fonction qui construit un objet. Si son utilisation dans les méthodes est possible selon le cas 2, celui-ci est régi par une autre règle :

Lorsqu’une fonction est appelée avec new, un nouvel objet est affecté (celui-ci reçoit d’aillers la propriété .prototype de la fonction appelée dans son slot [[Prototype]]), et cet objet est utilisé comme this lors de l’appel. C’est pour cela qu’il est possible de simuler new avec les fonctions :

function Hello() {
  this.a = 3;
}

const instance1 = new Hello();
instance1.a; // 3

// Équivalent à :
// On crée un objet avec comme prototype {Hello.prototype}
const instance2 = Object.create(Hello.prototype);
// On appelle le constructeur avec la bonne valeur de this
Hello.call(instance2);

instance2.a; // 3 :D

Dans ce cas, et dans ce cas précis, la fonction constructrice Hello dispose d’une valeur this définie, pointant sur l’instance à construire. Au contraire, si Hello est appelée sans new, alors on applique le cas 1 : this va pointer sur window/undefined, posant parfois d’énormes problèmes.

Traitement de "this" dans les fonctions fléchées

Maintenant que l’on sait comment se comporte this en fonction du contexe, il est temps d’aborder son cas dans les fonctions fléchées.

Le comportement entre les fonctions ordinaires et les fonctions fléchées est légèrement différent. Les fonctions fléchées n’ont pas leur propre valeur de this. La valeur de this à l’intérieur d’une fonction fléchée est toujours héritée depuis la portée englobante.

Avant de voir ce que ça donne en pratique, prenons un peu de recul. Les fonctions définies avec function reçoivent automatiquement une valeur pour this, qu’elles le veuillent ou non. N’avez-vous jamais écrit ceci ?

{
  ...
  addAll: function addAll(pieces) {
    var self = this;
    
    _.each(pieces, function (piece) {
      self.add(piece);
    });
  },
  ...
}

Ici, vous auriez voulu simplement écrire this.add(piece) dans la fonction interne. Malheureusement, la fonction interne n’hérite pas cette valeur de la fonction externe. À l’intérieur de la fonction interne, ce sera window ou undefined. La variable temporaire self sert à introduire la valeur externe de this dans la fonction interne (une autre solution est l’utilisation de .bind(this) sur la fonction interne ; aucune des deux n’est particulièrement jolie).

Avec les fonctions fléchées, cette bidouille ne sera plus nécessaire si vous respectez les règles suivantes :

Utilisez les fonctions non-fléchées pour les méthodes qui seront appelées par la syntaxe objet.méthode(). Ce sont les fonctions qui recevront une valeur significative de this via leur appelant ; Utilisez les fonctions fléchées pour tout le reste.

{
  ...
  addAll: function addAll(pieces) {
    _.each(pieces, piece => this.add(piece));
  },
  ...
}

Avec ceci, notez que la méthode addAll reçoit this à partir de sa fonction appelante. La fonction interne est une fonction fléchée, elle hérite donc du this contenu dans la portée englobante.

Il y a encore une différence mineure entre une fonction fléchée et une fonction "non fléchée" : la fonction fléchée ne reçoit pas d’objet arguments non plus.

Gênant ? Pas du tout ! Abordons tout de suite le remplaçant tant attendu du bancal arguments !

Les fonctions variadiques

Lorsqu’on crée une API, on a souvent besoin d’une fonction variadique, c’est-à-dire une fonction qui prend en entrée un nombre variable d’arguments. La méthode String.prototype.concat, par exemple, accepte autant de chaînes qu’on veut.

Grâce aux "paramètres du reste", JavaScript fournit une nouvelle façon d’écrire des fonctions variadiques.

Prenons cette fonction, qui vérifie si tous les paramètres (après le premier) sont des sous-chaînes du premier paramètre.

function contientSousChaines(source) {
  for (let i = 1; i < arguments.length; i++) {
    if (source.indexOf(arguments[i]) === -1) {
      return false;
    }
  }

  return true;
}

// On l'utilise comme ceci
contientSousChaines('source', 'so', 'our', 'r'); // true
contientSousChaines('source', 'so', 'our', 'cre'); // false

L’API de notre fonction n’est pas hyper intuitive : Dans sa définition, il apparaît qu’elle ne dispose que d’un seul paramètre (Est-il obligatoire d’ailleurs ? Un autre problème que l’on verra dans la prochaine partie).

Pourtant, pour l’utiliser, on a besoin de lui passer deux paramètres ou plus ! Ce manque de clareté est assez unique à JavaScript (la plupart des langages, même le C, disposent d’un opérateur ... à préciser pour indiquer une variadicité).

En 2015, ES6 introduit la syntaxe des arguments variadiques, qui remplace arguments.

La même fonction s’écrit comme ceci :

function contientSousChaines(source, ...substrings) {
  for (const substring of substrings) {
    if (source.indexOf(substring) === -1) {
      return false;
    }
  }

  return true;
}

// On l'utilise comme ceci
contientSousChaines('source', 'so', 'our', 'r'); // true
contientSousChaines('source', 'so', 'our', 'cre'); // false

Deux choses sont visibles : Dans la déclaration, on voit désormais que la fonction est variadique, et celle-ci stocke l’ensemble des arguments après source dans un tableau (classique) nommé substrings.

L’utilisation d’un tableau classique permet l’utilisation de l’itérateur sur tableau via for-of (arguments peut aussi proposer un itérateur, mais ce n’est pas standard).

Paramètres par défaut

Il y a autre chose avec les fonctions JavaScript qui ne semblait pas… juste. C’est l’utilisation de paramètres par défaut. En soi, ceux-ci n’étaient pas implémentés avec le langage, mais il se trouvait que tous les paramètres d’une fonction étaient optionnels.

Ainsi, écrire le code suivant est parfaitement valide :

function sayNames(person1, person2, person3) {
  console.log(person1, person2, person3);
}

sayNames(); // undefined undefined undefined

Lorsqu’un argument n’était pas spécifié, il recevait la valeur undefined, comme ça, sans problème. Ainsi, on pouvait gérer à l’intérieur de la fonction le cas où un argument valait undefined et, par exemple, lui assigner une valeur par défaut.

Là encore, ce n’est pas très clair : Quels arguments sont obligatoires et lesquels sont facultatifs dans nos fonctions ? En général, on devait regarder l’implémentation ou écrire un commentaire au dessus de la fonction.

C’est terminé ! Accueillons les paramètres par défaut.

function sayNames(person1 = 'Jimmy', person2 = 'Bob', person3 = 'Jack') {
  console.log(person1, person2, person3);
}

sayNames(); // Jimmy Bob Jack

Lorsqu’un argument n’est pas précisé, il est automatiquement affilié à sa valeur par défaut écrite dans la déclaration. Mieux encore, à la différence de Python, les expressions des valeurs par défaut sont évaluées lors de l’appel de la fonction, de gauche à droite. Cela signifie également que les expressions peuvent utiliser les valeurs calculées pour les paramètres précédents, tout comme on peut utiliser des expressions complexes, comme des appels de fonction ou des constructeurs.

function computeGroups(person1, person2, group = person1 + person2, at = new Date) {
  console.log(group, 'created at', at);
}

computeGroups('Jimmy', 'Bob'); // 'JimmyBob' 'created at' 2020/06/18

Si on passe explicitement undefined lors de l’appel, cela équivaut à ne rien passer du tout : Si le paramètre disposait d’une valeur par défaut, alors elle sera appliquée.

Si on ne passe pas un paramètre mais au contraire il n’a pas de valeur par défaut, alors (comme précédemment) il prendra la valeur undefined dans l’appel de fonction.

La fin

Cette partie est terminée !

La suivante parlera des différents ajouts syntaxiques au JavaScript au cours des années, et comment ils rendent le langage plus facile !

À bientôt 😀 !

Article précédent : Tableaux boostésArticle suivant : Ajouts syntaxiques

Laisser un commentaire