EcmaScript 6 Part 2


Ecmascript6

Bonjour à tous ! Voici la seconde partie de ma série d’articles sur ECMAScript 6 ! Ici nous aborderons les modules, qui doivent être familiers à la plupart d’entre vous. Si ce n’est pas le cas, ne vous inquiétez pas ! Je vais commencer par une piqûre de rappel. Étant donné l’énorme flow de nouveautés qu’apporte ECMAScript 6, je traiterai les sujets restant dans une troisième, puis quatrième et dernière (ouf !) partie.

Introduction

Plusieurs développeurs dit ‘backend’ (C#, Java, PHP, …) peste sur notre cher JavaScript avec des phrases du type : “Pas de namespace, pas de classe,… Impossible de découper son code en JavaScript. Je suis obligé de faire des gros pâtés ! Ce langage à dix ans de retard !”. Et bien primo, en utilisant certains mécanismes (closure, prototype, …) il est déjà possible d’avoir l’équivalent, même si oui ce n’est pas du tout intuitif pour un développeur non JavaScript. Secundo, il existe beaucoup de frameworks et d’APIs qui permettent de découper son code en modules. Et enfin tertio, avec ECMAScript 6 plus de mécanismes complexes, plus besoin d’API externe ! La syntaxe devient plus lisible et surtout plus ‘intuitif’ !

Le pattern module s’est dorénavant démocratiser dans le monde du JavaScript, mais avant l’existence de librairies (et framework) nous facilitant l’implémentation du pattern, comment qu’on faisait ? Je vais commencer par quelques rappels sur certaines notions indispensables pour comprendre comment implémenter ce pattern sous ECMAScript 5.

Les Module

A. Rappel closure

Rien ne sert de courir, il faut partir à point. Voilà pourquoi il est indispensable de bien cerner certaines notions propres au JavaScript avant d’entrer dans le vif du sujet. Une closure ? Kézako ? J’ai employé ce mot à plusieurs reprises dans mon premier article sans donner plus d’explications, mais pour pour bien comprendre les modules, on ne peut y échapper. Dès lors qu’une fonction en retourne une autre, c’est une closure ! Simple nan ? Bon un peu plus de précisions ne vous feraient pas de mal. C’est un mécanisme permettant de sauvegarder une partie du contexte parent en l’encapsulant dans une fonction fille qui sera retournée. La variable affectée à la fonction mère, récupère une référence sur la fonction fille. Toutes variables référencées dans la fonction fille (déclarée dans la fonction mère ou pas) n’est donc pas détruite par le garbage collector. En effet le garbage collector détruit uniquement tout élément n’ayant plus aucune référence. Si vous souhaitez en savoir plus sur le fonctionnement du ramasse miette, c’est ici.
Bon ce n’est pas forcement très claire dit comme ça, mais regardez ce petit exemple :

Souvenez-vous qu’en JavaScript, la portée d’une variable déclarée avec var est équivalente au block fonction dans lequel elle est déclarée. Dans l’exemple 1 ci-dessus, nous accédons à la variable (x) à travers sa fonction fille. Lors de sa création (myFirstClosure();) elle conserve la valeur de la variable x déclarée dans la fonction parent. Cela est encore plus flagrant dans le second exemple. Lors de l’appel à la fonction mère (mySecondClosure(5);), la fonction fille créée qui sera stockée dans la variable myFn2 garde une référence sur la variable x qui appartient au contexte parent. Elle a donc accès à sa valeur.

On pourrait faire un article entier (même plusieurs) sur le sujet. Donc si vous n’avez toujours pas compris le fonctionnement d’une closure (ce qui n’est pas un tort) je vous renvoie sur ce post ou bien celui-ci (plus original).

B. Implémenter le pattern module

Le but de ce pattern est principalement l’encapsulation. Cela apporte de nombreux avantages et évidemment quelques inconvénients. Premier avantage (et pas des moindre), cela apporte la possibilité de déclaré des données privées. Ce premier avantage apporte des inconvénients, car une méthode privée sera inaccessible depuis l’extérieur (en même temps, c’est un peu le but de la rendre privée) mais surtout il sera impossible de l’étendre à l’extérieur du module. Autres avantages, un gain de lisibilité et de clarté dans le code et surtout moins de collision de noms dans le scope global ! Nous allons maintenant voir les différentes implémentations de ce pattern :

1. IIFE avec closure

Je vous avez dit qu’on allait avoir besoin des closures ! Cette forme d’implémentation est utilisée par la plupart des framework que vous connaissez déjà (Jquery, YUI, Dojo, …). Si le mot IIFE vous est inconnu, ne vous inquiétez pas, car ce n’est clairement pas la chose la plus compliquer à comprendre. C’est tout simplement une fonction anonyme, affectée à une variable et qui est (comme son nom l’indique) immédiatement invoquée. Bon assez parlé voici un premier exemple de déclaration d’un module :

Comme vous pouvez le voir cela n’a rien de très compliquer même si la notation n’est pas forcément intuitive. Dans le premier exemple, on déclare un objet au sein d’une fonction IIFE qui sera retourné. Après l’exécution de la fonction, la variable myFirstModule obtient une référence sur l’objet obj. Les membres dit publiques sont accessibles via la variable myFirstModule contrairement aux membres privées qui malgrès tout persistent (en mémoire) grâce au mécanisme de closure (ils ne sont donc pas détruits après l’exécution de la IIFE). Il est donc possible d’utiliser des membres privés dans les membres publics, mais il est impossible de les appeler directement à partir de la variable myFirstModule. Le second exemple est une notation différente, ici on retourne un objet anonyme. Cette notation nous oblige à scroller (si le module est gros) pour accéder au return et voir les membres publics. C’est pour cette raison que je préfère personnellement la première notation. Bon, il nous reste à voir deux dernières petites choses. Comment le module peut accéder au monde extérieur et comment l’étendre. Par accéder au monde extérieur, j’entends accéder à des variables, méthodes,… déclarées à l’extérieur du module et que l’on souhaite utiliser au sein de ce même module. Pour cela rien de plus simple. Le IIFE qui retourne la référence du module peut prendre des paramètres et donc des données déclarées à l’extérieur du module ! Ba oui, c’est avant tout une fonction. Dans le cas où l’on souhaite étendre le module, il suffit de passer en paramètre de cette fonction le module lui-même. Bon un petit exemple pour illustrer le tout :

Cette technique est toujours d’actualité ! Par exemple en Typescript lorsqu’on utilise le mot-clé module le compilateur traduisant le code en JavaScript utilisera cette technique pour construire le module. Ayez ! Vous en savez assez pour attaquer le chapitre suivant qui parlera de deux APIs facilitant la création et la gestion de module.

2. AMD et CommonJs

Depuis quelques années le JavaScript explose. JavaScript côté serveur (nodeJs), MVC côté client (backbone, ember, angular, …), traitement de données complexes (D3Js),… Cela signifie une base de code js plus importante et donc la nécessiter de découper le code, le rendre plus lisible et plus réutilisable s’est logiquement imposé. La façon de gérer et déclarer les modules à également évoluer, grâce notamment à deux APIs : AMD et CommonJs. Elles font principalement la même chose, mais de deux manières différentes. Elles servent toutes les deux à déclarer des modules et surtout gérer les dépendances. Elles différents sur la façon de gérer les dépendances (en plus de la syntaxe), AMD les charges de façon asynchrone et CommonJs de façon synchrone. De par leur spécificité, on utilise généralement CommonJs côté serveur et AMD côté client.

La syntaxe CommonJs (utilisé en nodeJs) s’axe sur principalement sur deux mots-clés exports et require. Le premier mot-clé comme son nom l’indique permet d’exporter des données (méthode, objet, variable…). Le second permet de charger les données extérieures exportées à l’aide du mot-clé exports. Voici un exemple qui illustre l’utilisation de base de CommonJs :

Première question que vous devriez vous poser. Si l’on n’utilise pas l’implémentation du pattern module à l’aide d’une IIFE et closure dans les différents fichiers présentés dans l’exemple ci-dessus, tout est déclaré dans le scope global ? Et bien non ! Pour chaque fichier, un module est créé et donc les variables déclarées au sein de ce fichier sont rattachées à ce module et non au scope gloabal. Comment accéder aux métadatas de ce module ? J’ai oublié de vous parler d’un troisième mot-clé module. Il permet d’accès aux metadatas du module et possède également une méthode export qui vous permet d’exporter une propriété du module ! Cela peut s’avérer nécessaire, lorsque votre module ne contient qu’une seul donnée à exporter. Voici un exemple :

Ici je vous montre les bases, si vous voulez en savoir plus sur CommonJs je vous renvoie ici. Passons maintenant à la syntaxe AMD qui s’axe également sur deux mots-clés define et require. Le premier mot-clé define permet de définir un module. La signature de cette méthode est la suivante : define(id?, dependencies?, factory);. On précise en paramètre un identifiant (nom du module) puis les dépendances du module (s’il dépend de modules externes), puis la fabrique qui va retourner le module bâti. Voici un exemple d’utilisation :

Contrairement à CommonJs ou les modules sont défini implicitement, l’utilisation du mot-clé define rend la déclaration de module très explicite.
Le mot-clé require permet quant à lui de charger/utiliser des modules précédemment déclarées. Il charge les modules (et leurs dépendances) qui lui sont passé en paramètre puis exécute la callback défini. Voici un exemple d’utilisation :

Plusieurs librairies utilisent la syntaxe AMD pour vous permettre de déclarer et charger vos modules, pour citer les plus connues : RequireJs, CurlJs, Browserify, … Je n’irais pas plus loin dans l’explication sur AMD et CommonJs, mais il y a une chose que je n’ai pas précisée. CommonJs et AMD ne s’excluent pas forcément mutuellement. Ils peuvent être combinés pour déclarer et gérer l’ensemble des modules de votre application. L’un des grands avantages pour le JavaScript coté client, l’utilisation de ces APIs permettent d’utiliser des fichiers js sans avoir à les référencer dans une page HTML ! Bon vous devez sans doute vous dire, heuuu ton article c’est pas sur ECMAScript 6 normalement ? C’est exact, mais vous verrez que vous attaquerez plus facilement les modules ECMASCript 6 en ayant vu et compris la base de ceux-ci.

C. Module ECMAScript 6

1. Import et export

Les modules en ECMAScript 6 tirent le meilleur parti de CommonJS et AMD. Tout comme CommonJS, on y trouve une syntaxe simple permettant d’exporter les données d’un module (export) et une autre permettant l’import (import). Les modules en ECMAScript 6 visent à supporter le chargement asynchrone et synchrone. Chose qui peut vous paraître bizarre (nous en reparlerons) il permet également les dépendances cycliques ! Tout comme AMD, il supporte le chargement asynchrone et surtout la configuration de chargement des modules. Bon on attaque tout de suite avec un exemple :

Comme vous pouvez le voir cela ressemble fortement à la syntaxe CommonJS à quelque différence près. On utilise les accolades ou l’on y précise toutes les propriétés et méthodes du module que l’on souhaite importer. Il est ici possible de renommer les données importées comme on peut le voir avec la fonction doSomething qui devient myFn. Ensuite, on voit le symbole * qui permet de récupérer l’intégralité d’un module. Le mot-clé as permet quant à lui de nommer l’import effectué ceci afin d’éviter les collisions de nom. Mais les nouveautés ne s’arrêtent pas là ! En nodeJS, les modules dont on exporte seulement une propriété sont nombreux et c’est également le cas côté web. En ES 6, trouve donc l’équivalent du module.exports de CommonJS, le mot-clé default qui permet d’exporter une donnée par défaut d’un module. Voici un exemple :

Comme vous pouvez le constater avec la librairie otherCustomLib il n’est pas nécessaire de nommer la fonction qu’on exporte. Puisqu’elle est exportée par défaut lors de l’import on sait exactement ce que l’on récupère ! Bon maintenant voyons voir ce que ça donne lorsqu’on mixe le tout. Dans un même module nous allons exporter une donnée par défaut et exporter d’autres données :

Rien de très compliqué ! L’import par défaut et juste un import nommé :

Autre particularité dont je n’ai pas parlé, sachez qu’il est possible de nommer ses exports. Pourquoi ? Tout simplement pour faciliter au maximum la lisibilité de ce qui est exporté et de ce qui importé. Il est également possible de ré-exporter un module à partir d’un modules qui l’a importé. Voici un exemple :

Je ne vous montrerai pas d’exemple mais sachez qu’il également possible d’importer un module depuis une url. Une dernière petite chose, on accède aux metadatas (nom du fichier, url, …) d’un module en utilisant le mot-clé this (équivalent au mot-clé module en CommonJS). Voici un exemple d’utilisation :

Bon assez parler de la syntaxe d’export/import. Passons à un chapitre un peu plus théorique !

2. Résolution statique et dynamique

Si je vous parle de résolution statique des modules, ça vous parle ? Bon voyons déjà la différence entre résolution à la compilation et à l’exécution :

  • Compilation : dans un premier temps le moteur (du navigateur) analyse le code JavaScript. Il repère les modules et les mots-clés import et export qu’ils contiennent. Les dépendances des différents modules sont récupérés puis analysés à leur tour. Lorsque l’arbre des dépendances d’un module et entièrement parcouru, le système qui charge les modules (module loader) va câbler l’export de ce module ainsi que ses imports au sein d’autres modules en vérifiant le type de ce qui est exporté et importé puis en vérifiant comment il est importé. Dernière étape, le code est évalué puis exécuté.
  • Exécution : lors de la résolution à l’exécution, il n’y a tout simplement pas cette phase de compilation et donc d’analyse, de résolution, de vérification puis d’exécution. Dès lors qu’un module est rencontré par le moteur, il est exécuté. C’est comme cela que fonctionnent actuellement les systèmes de modules sous ECMAScript 5. Mis à part une petite exception avec AMD où une fonction utilisant require passera par une phase d’analyse.

Les modules d’ECMAScript 6 utilisent la première solution présentée et donc la résolution static des modules, c’est-à-dire à la compilation. Que cela peut-il bien apporter ?

  • Meilleure performance : en effet avec la résolution lors de la compilation, les données importées sont traduites en référence pointant directement sur les méthodes, propriété qu’elles représentent. Si la résolution des modules se passe lors de l’exécution, les données sont importées sous forme d’objet. Lors de l’utilisation de la propriété d’un objet, le moteur doit donc rechercher au sein de l’objet (puis dans sa chaîne de proptoype) la propriété appelée.
  • Vérification de type : cette structure static des modules permet l’arrivée de la phase de vérification des types beaucoup plus tôt. Permettant ainsi de détecter des erreurs lors du binding des imports/exports. Cela permet également de déterminer la portée des variables au sein du module.
  • Chargement synchrone et asynchrone des modules : la syntaxe utilisée pour les modules ECMAScript 6 ressemble beaucoup à celle de CommonJS et pour cause, le système est construit pour le chargement synchrone des modules. Mais pas seulement, car la résolution statique des modules permet également le chargement asynchrone ! Puisque tous les imports sont résolus avant l’exécution, il est possible de charger les dépendances avant de charger le corps du module qui les importe.
  • Laisse la porte ouverte aux futures évolutions : une évolution attendue par beaucoup de développeurs et l’arrivée des Macro ! Il serait ainsi possible d’étendre la syntaxe du langage et le comportement de la syntaxe actuelle. Les modules d’ECMAScript 6 offrent un socle prêt à accueillir cette nouveauté. Pour pouvoir, importer et utiliser des macro le moteur JavaScript doit passer par une phase de “pré-processing” (avant l’exécution) afin de résoudre les imports pour pouvoir déterminer si le code à évaluer puis exécuter contient des macros ou pas. Bon on ne s’enflamme pas, les macro sont prévues pour ECMAScript 8 ! Autre évolution attendue, le typage. Le but n’est pas de transformer le JavaScript en langage fortement typé, mais de pouvoir utiliser des variables typées (et définir ses propres types) lorsque cela est nécessaire, notamment afin d’optimiser l’espace mémoire occupé et ainsi accroître les performances des programmes. Pour bien comprendre l’utilité du typage, je vous renvoie sur cet article. Et pour ceux qui pensaient que le typage static en JavaScript ça n’existe pas, jetez un coup d’œil à LLJS.
  • Possibilité d’utiliser des dépendances cycliques : non, non, vous avez bien lu. Ne vous frottez pas les yeux, j’ai bien dit dépendances cycliques ! Alors oui dans les bonnes pratiques, il faut éviter au maximum les dépendances cycliques entre deux modules, car cela les rend fortement couplées. Les deux doivent alors évoluer simultanément et trop de dépendance cyclique nous amène à un gros sac de nœuds ou lorsqu’on touche une portion de code dans un module, on pète tout dans les autres. Mais cela est parfois nécessaire ! Si l’on prend l’exemple de l’arbre du DOM, un parent à la référence de son enfant et l’enfant à la référence de son parent. Lorsqu’on découpe un gros module en plusieurs, il peut arriver qu’on passe par des dépendances circulaires.

Tous ces bénéfices sont dus à la résolution static des modules ! Mais ce n’est pas tout. Il existe également une API qui facilitera la création et la configuration du chargement des modules dont nous allons parler dans le prochain chapitre.

3. API de chargement des modules

Cet élément ne fait pas encore partie du brouillon des spécifications ECMAScript 6. Vous trouverez la dernière mise à jour ici. Le but est d’avoir un loader embarqué qui se chargera de créer, charger,… les modules dans des contextes isolés et contrôlés. De nouveaux loader peuvent être créés via un constructeur Loader : function(options = {}) -> Loader. Le code sera compilé par un loader spécifique et sera lié (statiquement car à la compilation) de façon permanente à ce loader. Cette association sera utilisée pour récupérer des modules externes, charger du code dynamiquement (eval()) et récupérer les variables globales. Mais ce n’est pas tout. Il prévoit également un constructeur Module qui permet de créer des modules ainsi un qu’une méthode ToModule qui permettrait de convertir un objet en module. Voici un petit exemple avec des utilisations du loader par défaut et création de modules :

Sachez que ce fonctionnement offre beaucoup de possibilités dont je n’ai pas encore parlé. Il y a des points d’extension dans l’ensemble de la chaîne de récupération d’un module (ou script). Il est donc possible de créer un loader custom qui va traduire à la volée les modules (et scripts) TypeScript ou CoffeScript chargé, ou bien encore appliquer le strict mode, appliquer jshint,… Je ne parlerais pas plus de ce système, je vous laisse aller lire les spécifications et je vous renvoie sur ce très bon article décrivant l’architecture des modules ECMAScript 6.

Conclusion

J’ai abordé beaucoup de sujets sans aller trop en profondeur et j’espère que cela a titillé votre curiosité ! N’hésitez pas à approfondir les points abordés, pour votre culture personnelle et puis pour me faire des retours sur votre compréhension du sujet qui peut être différent de la mienne. Nous en avons terminé avec les modules. Dans le dernier article, je donnerai une série de liens vers des compilateurs ES6->ES5 et langage compilé en JS où il est déjà possible d’utiliser la nouvelle syntaxe des modules ! Dans la prochaine partie, nous attaquerons un grand sujet : les classes.

Commentez cet article