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

Cet article fait partie de la série “Pour les Jedis, JavaScript” d’articles consacrés à JavaScript. Si ce n’est pas déjà fait, veuillez lire les épisodes précédents.
  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)

Comme vous le savez tous, JavaScript est le langage maître du Web. Il est à la fois simple et puissant. Je ne vous cache pas ma fascination pour ce langage et son écosystème; et cela dure depuis 2004. C’est pour cette raison que j’ai décidé de me consacrer à la rédaction d’une série d’articles concernant les fondamentaux de JavaScript, un langage très populaire certes mais en même temps encore méconnu. Ces articles auront pour but de vous expliquer en détails le fonctionnement de ce langage afin de faire de vous des JavaScript Jedis.

En effet, grâce à cette série d’articles, nous allons apprendre et comprendre JavaScript afin de tirer le meilleur de ce langage. Je trouve cela injuste de voir beaucoup de mes camarades développeurs traiter ce langage de tous les noms car ils n’arrivent simplement pas à faire ce qu’ils ont l’habitude de produire avec leurs langages favoris. Par conséquence, ces personnes finissent donc par développer une sorte de haine contre JavaScript.

“JavaScript, tu l’apprends ou tu le quittes.”

Vous vous demandez sûrement pourquoi nous commençons cette série par les fonctions, et non pas par les objets. Eh bien ! Étant donné la nature fonctionnelle de JavaScript, pour moi, maîtriser cet aspect du langage c’est ce qui fera de vous des Jedis. En effet, la plupart des développeurs venant d’autres langages orientés objet, essayent plus de reproduire des paradigmes propres à ces langages et font l’impasse sur les fonctions et les fermetures (Closures). JavaScript est composé de ces trois concepts : les objets, les fonctions et les Closures. Maîtriser JavaScript passe par l’apprentissage de cet aspect fonctionnel du langage ; Et croyez-moi, le niveau de sophistication du code que vous écrirez dépend de cet apprentissage.

Disclaimer : Cette série d’articles n’a pas pour vocation d’expliquer les nouveautés d’ES2015, mais plutôt le fonctionnement de JavaScript. Les nouveautés d’ES2015 feront l’objet d’un prochain article.

JavaScript est un langage fonctionnel

L’une des raisons qui fait que les fonctions et la nature fonctionnelle de JavaScript sont des concepts très importants, vient du fait que « la fonction » est le premier module d’exécution en JavaScript. C’est-à-dire que lorsque votre code est exécuté, il l’est au sein d’une fonction ; à l’exception des scripts qui sont interprétés lorsque le code HTML est en cours d’être évalué par le navigateur. Je fais évidemment allusion à l’emblématique document.write() qui permettait de créer du DOM au runtime, obligeant le navigateur à bloquer l’interprétation du code HTML.

Revenons à nos moutons, une chose très importante à connaître sur les fonctions en JavaScript, ce sont des objets de premier ordre (first-class objects). Ceci veut dire que les fonctions sont traitées comme des objets, elles peuvent donc être :

  • créées via des littérales ;
  • assignées à des variables ou propriétés d’un objet ;
  • passées en paramètre ;
  • retournées comme résultat ;
  • elles peuvent posséder des objets ou méthodes.

En plus d’être traitées comme des objets, les fonctions ont également une “fonction” spéciale : elles peuvent être invoquées (on en reparlera plus en détails dans la suite de cet article). Cette invocation est souvent effectuée de manière asynchrone. Voici pourquoi…

La boucle d’événements

Si vous avez déjà codé une interface graphique auparavant, vous avez sûrement procédé comme ceci :

  • mettre en place l’interface graphique ;
  • se mettre dans une boucle d’événements en attente qu’un événement se produise ;
  • invoquer les actions à exécuter pour chaque événement.

La programmation en JavaScript — dans un navigateur — est quasiment similaire, sauf que la gestion des événements est totalement prise en charge par le navigateur. Nous avons juste besoin de spécifier les actions (listeners) pour les différents événements qui peuvent être déclenchés — toujours au sein du navigateur. Ces événements sont placés dans une file au fur et à mesure qu’ils se produisent, et le navigateur traite ces événements en invoquant les actions associées. A noter que ce principe reste identique pour d’autres environnements JavaScript, par exemple Node.js.

Puisque ces événements peuvent se produire à n’importe quel moment et dans n’importe quel ordre, la gestion de ces mêmes événements, et donc l’invocation de leurs actions, se fait de manière asynchrone. Pourquoi me diriez-vous ?

Une chose à savoir, la boucle d’événements du navigateur est “mono-threadée”, ce qui veut dire que chaque événement se trouvant dans la file d’attente, est traité dans l’ordre d’arrivé (FIFO). Les événements sont donc traités un par un, à tour de rôle. Voici un schéma très simplifié de ce processus :

La boucle d’événements du navigateur

Un exemple typique du fonctionnement de cette boucle : lorsque vous bougez le curseur de votre souris dans votre page web, le navigateur détecte ces déplacements de souris et place cette série d’événements (mousemove) dans la FIFO. Ensuite, la boucle d’événements s’occupera du traitement de ces événements, en exécutant les actions associées.

Ce principe d’actions — associer une fonction qui sera exécutée plus tard, lorsque l’événement se produira — illustre un mécanisme que l’on appelle les “fonctions de callback”.

Le principe de callback

Les fonctions de callback sont une partie très essentielle dans la bonne compréhension de JavaScript. Explorons ce point…

Dans la partie précédente, nous avons dit que l’on pouvait associer des actions aux différents événements se produisant au sein du navigateur. Voici un exemple d’une action :

Ici, nous avons attaché une action (listener) qui sera exécutée lorsque la page est complètement chargée par le navigateur. Parce que les fonctions sont des first-class object, nous pouvons affecter cette fonction à la propriété onload de l’objet window (représentant le navigateur).

Aussi, nous avons dit que les fonctions peuvent être passées en paramètre. Ce qui veut dire que l’on peut écrire ceci :

Voilà un exemple intéressant ! Nous pouvons aussi appeler les fonctions de callback dans nos propres fonctions, pas besoin qu’elles soient associées à des événements. Dans cet exemple, nous avons attaché une fonction de callback à l’événement onload. Lorsque la boucle d’événement exécutera ce listener, à son tour il exécutera la fonction dire à laquelle nous avons passé la référence vers la fonction bonjour. Une fois exécutée, la fonction dire invoquera la fonction bonjour ce qui provoquera l’affichage du message “Bonjour, JavaScript !”.

Mieux encore ? La nature fonctionnelle de JavaScript nous permet de créer des fonctions en tant qu’entité à part entière, comme n’importe quel autre type, et les passer directement en paramètre. Voyons un exemple :

Nous avons modifié l’exemple précédent en supprimant la déclaration de la fonction bonjour. Nous l’avons remplacé par la déclaration d’une fonction sans nom, directement dans les paramètres de la fonction dire. En JavaScript, nous appelons ce genre de fonctions, les fonctions anonymes.

Les fonctions anonymes

Étant donné la nature fonctionnelle du langage, il possible de créer des fonctions n’importe où dans le code — là où une expression est attendue. Cela a pour avantage de rendre le code plus claire, plus compact et en plus, cela permet d’éviter de polluer l’espace de nom globale avec des noms de fonctions inutiles.

En effet, une fonction anonyme est une fonction — comme les autres — déclarée en in-line, et qui n’a pas de nom pour la référencer. Ces fonctions anonymes sont généralement déclarées au moment où elles sont référencées.

Généralement en JavaScript, lorsque nous estimons qu’une fonction va être référencée dans plusieurs endroits du code, nous lui donnons un nom. Sinon, si elle est destinée à servir juste à un seul endroit, alors pas besoin qu’elle ait un nom. Ce qui nous amène aux déclarations et invocations des fonctions.

Déclaration de fonctions

En JavaScript, les fonctions sont déclarées à l’aide du mot-clé function qui crée une valeur de type fonction. Rappelez-vous que les fonctions sont des objets de premier ordre, elles peuvent être manipulées dans le langage comme n’importe quel autre type. De ce fait, il nous est possible d’écrire ceci (en reprenant l’exemple précédent) :

La valeur de la variable dire est une fonction ! Nous avons donc déclaré et créé une fonction anonyme référencée par la variable dire.

Toutes les fonctions déclarées ont une propriété name qui contient le nom de la fonction (de type String). Pour les fonctions anonymes, cette propriété est présente mais elle est vide.

Lorsqu’une fonction a été déclarée avec un nom, ce nom reste valide dans tout le contexte dans lequel cette fonction a été déclarée. Aussi, si cette fonction a été déclarée dans le contexte globale, une nouvelle propriété portant le nom de la fonction est ajouté dans le window ; ceci est également vrai pour les variables. Ceci nous amène donc à la suite de cet épisode concernant le contexte et la portée des fonctions.

Portée et contexte(s) d’une fonction

Lorsque nous déclarons une fonction, nous devons avoir à l’esprit, non seulement le contexte dans lequel cette fonction existe, mais également quels sont le ou les contextes que cette fonction crée. Nous devons également faire très attention aux déclarations faites au sein de ces contextes.

La gestion des portées en JavaScript est très différentes de la plupart des autres langages dont la syntaxe a été influencée par le langage C. Plus précisément, ceux utilisant les accolades pour délimiter la portée des variables dans les blocs. Dans ces langages, chaque bloc crée son propre contexte. Ce n’est pas le cas de JavaScript.

En JavaScript, les contextes sont créés par les fonctions, et non pas par les blocs.

La portée d’une variable créée au sein d’un bloc continu d’exister en dehors de ce dernier. Par exemple :

Ce simple exemple prouve bien que la portée de la variable foo est globale, elle n’est pas liée au bloc if. Regardons un exemple un peu plus intéressant :

Dans cet exemple, nous avons déclaré deux fonctions, fonction1 et fonction2, ainsi que trois variables, a, b et c. Pour compléter cet exemple, voici un schéma plus parlant qui explique les différents contextes créés :

La porté des variables en JavaScript

Ce schéma résume les règles suivantes :

  • la portée d’une variable existe depuis sa déclaration et ce jusqu’à la fin de la fonction dans laquelle elle a été déclarée, peu importe l’imbrication des blocs (variables declaration hoisting);
  • la portée d’une fonction (non-anonyme) est globale à toute la fonction dans laquelle elle a été déclarée (functions declaration hoisting).

Cette deuxième règle nous montre que, contrairement aux variables, les fonctions peuvent être référencées avant leurs déclarations ! Oui, exactement, vous pouvez invoquer fonction1 avant de la déclarer. Par contre, ce n’est pas une bonne pratique. Ce n’est pas parce que c’est autorisé par le langage qu’il faut le faire.

Il serait intéressant de noter que l’introduction des mots-clé let et const depuis ES2015 a permis de corriger bon nombres de problèmes liés à la porté des variables en JavaScript. Mais cela sera l’objet d’un autre article.

Maintenant que nous avons vu comment les fonctions sont déclarées, essayons de voir comment elles peuvent être invoquées. Attention, cela risque d’être très amusant…

Invocation des fonctions

Nous avons forcément tous invoqué des fonctions en JavaScript, au moins une fois dans notre vie. Mais avez-vous déjà essayé de comprendre ce qui se passe lorsque vous invoquez une fonction ? Savez-vous qu’il existe quatre façons d’invoquer une fonction en JavaScript ? Chaque type d’invocation a un effet direct sur le contexte d’exécution de cette fonction. Plus précisément, le type d’invocation agit sur le paramètre this passé à la fonction lors de son invocation. Ecrire du code digne d’un Jedi, repose sur la compréhension et la bonne maîtrise de ces mécanismes d’invocations.

Voici les différents types d’invocation en JavaScript :

  1. en tant que fonction (c’est le cas le plus répandu) ;
  2. en tant que méthode, ce qui permet de lier l’invocation à un objet (POO) ;
  3. en tant que constructeur, ce qui permet de créer un objet (instance) ;
  4. via apply() et call() (expliqué plus loin).

Avant d’expliquer chaque type d’invocation, prenons quelques minutes et essayons de comprendre ce qui se passe lors d’une invocation de fonction (cela est vrai pour les cas 1, 2, et 3).

Arguments et paramètres

Lorsqu’une liste d’arguments est fournie lors d’une invocation de fonction, ces arguments sont affectés aux paramètres (spécifiés lors de la déclaration) de la fonction, dans le même ordre. Jusque là rien de bien sorcier.

Maintenant, si le nombre d’arguments est différent de celui des paramètres, aucune erreur n’est déclenchée : JavaScript sait comment traiter ce cas :

  • s’il y a plus d’arguments que de paramètres, alors l’excédent est ignoré ;
  • s’il y a moins d’arguments que de paramètres, alors les paramètres qui n’ont pas d’arguments seront mis à undefined.

De plus, au moment de l’invocation, deux paramètres additionnels sont passés implicitement : this et arguments. Autrement-dit, ces deux paramètres sont passés à la fonction qui est invoquée, même s’ils ne sont pas spécifiés en tant que paramètres dans la déclaration. Ils sont par la suite disponibles dans le corps de la fonction et peuvent être référencés comme n’importe quels autres vrais paramètres.

L’argument “arguments”

Cet objet est une sorte de collection de tous les arguments passés à la fonction.

Mais attention, même si cet objet possède un attribut length, et permet d’accéder aux arguments via une notation comme pour un tableau arguments[i], ce n’est pas un tableau à proprement parler. Vous ne pouvez pas utiliser les méthodes spécifiques aux tableaux, comme map() ou filter() par exemple ; autrement dit, il n’est pas possible d’itérer sur l’objet arguments. Cependant, il existe des astuces permettant de convertir ce type de collection en un tableau. Parmi ces astuces, nous pouvons citer :

L’argument “this”

Cet argument est un peu particulier. Il représente l’objet qui est associé — de façon implicite — à l’invocation de la fonction. Il est aussi connu sous le terme de contexte. Le terme contexte est bien connu des développeurs habitués au développement orienté objet. Cependant, dans la majorité des cas, ces mêmes personnes pensent qu’en JavaScript, le mot clé this représente l’instance de la classe dans laquelle la méthode est définie. Ce n’est pas le cas !

En JavaScript, il se trouve que le contexte représenté par le paramètre this est conditionné par la façon dont la fonction a été invoquée, et non pas par la façon dont elle a été définie. C’est pour cela que ce paramètre est appelé le contexte d’invocation. Soyez donc vigilants !


Maintenant que nous avons bien compris ce que c’est this et ce qu’il représente. Dans la seconde partie de cet article, nous allons expliquer les différents types d’invocations des fonctions en JavaScript.

Que le JS soit avec vous…


Avez-vous aimé cet article ? Soutenez-moi avec 50 claps et Suivez moi sur Medium et Twitter pour plus de contenu sur JavaScript et le Web 🎉