Nouvelles Chroniques d'Amethyste

Penser au Sens, pas au Verbe

Le pattern module en JavaScript

Poster un commentaire

Le pattern module est un des patterns les plus importants en JavaScript puisqu’il est la brique de tous les frameworks utilisés. Il est bon d’en comprendre le fonctionnement et ce n’est pas simple car il s’appuie sur une série de concepts parfois assez subtils à comprendre:

J’ai déjà décrit ces outils dans des articles précédents, je vais essayer de monter comment on les met en œuvre pour mettre en place des modules.

A la fin je consacre aussi un chapitre sur les nouveautés de ES6 sur ce sujet.

Edit: j’ai rédigé une suite à cet article ici:

https://amethyste16.wordpress.com/2017/06/24/les-architectures-de-modules-en-javascript/

 

Pourquoi?

L’objectif de tout patron de conception est de résoudre un besoin précis. Si par exemple je regarde le code suivant un peu caricatural:

var message="Hello Amethyste";
 
var afficherMessage = function() {
   alert(message);
}
 
afficherMessage();

 

Ce code fonctionne parfaitement, il serait seul dans l’application il n’y aurait sans doute pas grand chose à en dire. Mais justement, il n’est peut être pas seul.

Comme expliqué dans cet article sur la notion de scope (portée), les deux paramètres message et afficherMessage vont être déclarés dans le scope global qui est en général l’objet window.

On est alors pas à l’abris que ces variables soient redéfinies par un autre code, un quelconque Framework JavaScript utilisé par l’appli. De plus il n’est pas évident que l’on souhaite rendre public tous les paramètres, toutes les méthodes utilisées, ne serait-ce que pour rendre lisible le code que vous avez produit.

 

Dans les autres langages on protègerait et organiserai ce code avec des packages…, mais on ne dispose de rien de tel en JavaScript. L’équivalent le plus proche est précisément le module. Ce n’est pas un concept géré tel quel par JavaScript, encore que ES 6 apporte des avancées, mais c’est le résultat de la mise en œuvre de plusieurs mécanismes de base du langage.

Je vais essayer de décortiquer tout ça dans cet article.

L’outillage

Le code ne s’apprend pas devant un écran, mais devant son clavier. Donc faites tourner tous les exemples proposés. Faites le avec l’outil de votre choix, pour ma part j’ai choisit Plunkr.

Le scope

Le scope, la portée, c’est l’ensemble des règles mises en œuvre par un langage pour résoudre une variable. Ces règles définissent le périmètre de visibilité de chaque variable. Chaque langage a ses règles à lui.

 

La brique de base pour créer un scope local en JavaScript est la fonction, pas juste les accolades ({…}).

var fonctionExterne = function() {
   var x =1;
}

La variable x est accessible partout dans fonctionExterne, mais pas à l’extérieur.

Un cas particulier est celui ou fonctionExterne contient elle-même d’autres fonctions qui peuvent avoir ou pas des déclarations de variables comme dans l’exemple qui suit:

 

var fonctionExterne = function() {
   var x =1;
 
   var fonctionInterne = function() {
      var y=2;
 
      console.log(x);
      console.log(y);
   }
 
   return fonctionInterne;
}
 
 
var maFonction = fonctionExterne();
maFonction();

 

La règle est simple:

X est accessible de partout à l’intérieur de fonctionExterne, mais pas à l’extérieur. Ca n’a pas changé.

Y n’est accessible que depuis fonctionInterne.

 

Ce qui définit le scope d’un paramètre est sa position dans le code (à l’intérieur ou non de fonctionExterne par exemple). Lorsque JavaScript a besoin de résoudre un paramètre, il part du scope le plus interne et remonte de proche en proche les scopes jusqu’à trouver une définition de la variable. Regardons cet exemple pour clarifier les choses:

var x=2;
 
function fonctionExterne() {
   var x =1;
   console.log(x);
}
 
 
fonctionExterne();
console.log(x);

Il s’affiche successivement: 1 puis 2.

Pourtant x a été déclaré deux fois, mais dans un scope différent. Lorsque JavaScript exécute la ligne 4, il a besoin de résoudre x. Il recherche la première déclaration de type:

var x

En remontant depuis son scope courant. Il n’a pas à aller loin, la ligne 4 elle même est candidate. La même déclaration ligne 1 n’est pas concernée et ne voit pas son contenu pollué par la déclaration:

x=1

 

Cela signifie que le scope d’une fonction interne contient le scope de sa fonction parente. On dit que JavaScript (et la grande majorité des langages de programmation)  utilise un scope lexical.

Une conséquence est que les scopes sont nécessairement entièrement imbriqués ou entièrement séparés.

On ne peut pas avoir à la façon des diagrammes de Venn deux scopes S1 et S2 tels que S1 englobe une partie des variables de S2, mais pas toutes.

 

IIFE

Relisez le dernier exemple. On a certes masqué une variable x devant le scope global, mais en échange on a ajouté quelque chose à ce scope qui n’y était pas avant. On a donc pas vraiment résolu le problème de la collision des noms de variables.

JavaScript procure un moyen de déclarer cette fonction sans lui donner un nom, en tout cas sans lui donner un nom qui va polluer le scope global. On modifie le code ainsi:

var x=2;
 
(function fonctionExterne() {
   var x =1;
   console.log(x);
})();
 
console.log(x);

J’ai fait une assez longue description de cette structure dans un article précédent et ne vais pas la reproduire ici. Juste apporter quelques commentaires.

 

Tout d’abord l’affichage est exactement le même. Simplement il n’y a plus de déclaration pour fonctionExterne accessible depuis le scope global. La fonction a pourtant bien été définit, où est elle déclarée?

Dans son propre scope! La fonction se cache dans elle-même.

Bien entendu il se pose la question de l’accessibilité des méthodes ou des variables qu’elle expose. Nous résoudrons le problème lorsque l’on parlera des modules.

 

Note: Il n’est pas obligatoire de donner un nom à une IIFE. Nous verrons plus loin des exemples.

Closure

Le plus compliqué avec les closures, c’est de les voir. Une fois pigé le truc on en voit partout, parce que justement il y en a partout… et dans tous les langages, ce n’est pas qu’un concept JavaScript.

Examinons cet exemple:

function wait(message) {
   setTimeout(function timer() {
      console.log(message);
   },1000);
}
 
 
wait("Hello Amethyste!");

On a une méthode wait() qui attend un message dans ses paramètres. Le message est affiché dans la console  par la méthode interne timer(), mais après un délai d’une seconde après l’exécution de wait.

1 seconde c’est largement suffisant pour exécuter wait et revenir. Normalement le scope définit par timer devrait avoir été nettoyé depuis longtemps. Hors ce n’est pas ce que nous observons puisque timer réussi à retrouver le contenu de la variable message pourtant définit au niveau de wait.

Pour décrire ce comportement on dira que timer a une closure sur le scope de wait.

Une closure est la capacité d’une fonction de retrouver et accéder à son scope, même si elle s’exécute en dehors de ce scope.

Dans notre exemple, la fonction est timer et son scope est wait. On voit bien qu’elle va s’exécuter bien après que l’on a quitté wait. Et pourtant elle se souviendra de son scope et en particulier de la variable message et son contenu.

 

Les boucles sont une autre source de closure dès que l’on y définit une fonction.


for (var i=1;i<5;i++) {
   setTimeout(function timer() {
      console.log(i);
   },1000);
}

Je pense que vous savez tous qu’il ne va pas s’afficher les nombres de 1 à 4, mais 4 fois le nombre 5.

 

La fonction timer est exécutée bien après que la boucle se soit terminée. Donc au moment de son exécution, la valeur de i sera 4 et non pas 1, 2, 3 et 4.

C’est ce qui explique le comportement observé.  Le point crucial à observer et que l’on a qu’un seul scope pour lequel i est définit. Il n’y a pas un scope par itération. C’est ce scope qui est l’objet de la closure.

 

Pour avoir le comportement espéré, on a donc besoin de scopes supplémentaires, un par itération précisément. Comment faire?

La première fois que j’ai été confronté au problème, j’ai tout de suite pensé à cette solution:

for (var i=1;i<5;i++) {
   var j=i;
   setTimeout(function timer() {
      console.log(j);
   },1000);
 
}

Ce qui ne marche pas et c’est évident puisque l’on a rien changé au problème qui précède. En JavaScript les accolades ne forment pas un nouveau scope, ce sont les fonctions.

En deuxième idée on peut penser aux IIFE:

 


for (var i=1;i<5;i++) {
   (function () {
         setTimeout(function timer() {
            console.log(i);
         },1000);
   })();
}

 

Je pense que nous avons tous tenté ceci et constaté que ça ne marche pas, bien que cette fois on ai 4 scopes.

Ca ne marche pas parce qu’il y a une deuxième condition: le scope ne doit pas être vide. C’est le cas ici, rien n’a été définit dans le scope, il ne fait rien de spécial. Pour ne plus avoir de scope vide il suffit de déclarer une variable intermédiaire:


for (var i=1;i<5;i++) {
   (function () {
      var j=i;
      setTimeout(function timer() {
      console.log(j);
      },1000);
   })();
}

Cette fois ça marche. Une variante possible est de passer i directement comme paramètre de l’IIFE, on n’a alors pas besoin de la déclaration intermédiaire.

 

Si vous testez le code avec des outils genre Lint, vous remarquerez qu’ils râlent un peu. C’est pourtant une façon correcte de gérer le problème de closure, il est juste pas assez intelligent pour s’en rendre compte. Donc ignorez le sur ce point!

Les modules

Nous voici donc armés pour atteindre l’objectif de cet article: la création de modules en JavaScript.

En JavaScript un module est une façon de créer une sorte de boîte dans laquelle on pourra embarquer une collection de méthodes ou de variables liées entre elles qui forment un Framework. C’est la boîte, le module, qui détermine l’accessibilité de chaque membre. Pour le code qui la consomme, il s’agit donc d’une boîte noire.

 

Revealing module

La première idée que l’on pourrait avoir est celle-ci:

var toto = {
   fonction1 : function fonction1() {
      console.log("On est dans fonction 1");
   },
 
   maFonction2 : function fonction2() {
      console.log('On est dans fonction 2');
   }
}
 
toto.fonction1();
toto.maFonction2();

En apparence le code répond à la définition d’un module à un détail près: comment fait t’on pour définir des membres privés?

Pour accéder à la notion de membre privé on a besoin d’une fonction. En JavaScript, un module sera donc forcément une fonction.

 

Le plus souvent, le module est introduit sous la forme dite du revealing module. Qui est très simple:

function monModule() {
   var message = "Hello Amethyste";
   function fonction1() {
      console.log("On est dans fonction 1");
   }
 
   function fonction2() {
      console.log('On est dans fonction 2');
   }
 
   return { // accolade sur la même ligne!
      fonction1:fonction1,
      maFonction2:fonction2
   }
}
 
var toto=monModule();
toto.fonction1();
toto.maFonction2();

Avons nous un module?

On créée un scope avec monModule(). Plusieurs méthodes ou variables y sont définis et de fait isolé de tout scope parent comme le scope global.

L’appel à monModule déclenche le code ligne 11 qui consiste à renvoyer une instance d’un objet qui donne accès à certains membres du module, mais pas tous.

Les lignes 17, 18 et 19 montrent qu’il est possible de consommer le module depuis l’extérieur les deux méthodes qui sont donc public. Par contre on a une variable private: message.

Les intentions du pattern module sont toutes remplies, il s’agit donc bien d’un module.

 

Note: un module est une fonction. Il est donc parfaitement possible de construire des modules paramétrés en déclarant des paramètres à la fonction.

 

On est pas obligé de renvoyer un objet, on pourrait retourner directement une fonction:

function monModule() {
function fonction1() {
   console.log("On est dans fonction 1");
}
 
return fonction1;
}
 
var toto=monModule();
toto();

C’est ce que font certains frameworks comme JQuery.

Module comme un singleton

L’exemple va créer une instance différente du module à chaque fois que l’on écrira une ligne telle la ligne 17. Parfois on voudrait plutôt un singleton. Les IIFE vont résoudre la question:

var toto = (function monModule() {
   function fonction1() {
      console.log("On est dans fonction 1");
   }
 
   function fonction2() {
      console.log('On est dans fonction 2');
   }
 
   return {
      fonction1:fonction1,
      maFonction2:fonction2
   }
})();
 
toto.fonction1();
toto.maFonction2();

Une autre méthode

Angular a popularisé un autre schéma de construction des modules.

Le cœur de ce schéma est la méthode Apply() qui est similaire à Call(). Je vais reprendre la description trouvée ici:

var x = 10;
 
function f()
{
   alert(this.x);
}
 
f();

On a déclaré une fonction globale f que l’on appelle en ligne 8. On remarque la présence de this. La fonction n’étant pas appelée depuis une instance d’un objet, this ne peut que représenter le scope global.

Apply (et Call) vont nous permettre de définir vers quoi pointe this au moment de l’invocation de la méthode sur toute sa durée. Par exemple:

var x = 10;
var o = { x: 15 };
 
function f()
{
   alert(this.x);
}
 
f();
f.call(o);

Va afficher successivement 10, puis 15.

Lors de la première invocation, this est le scope global. C’est donc là que JavaScript va chercher la valeur de x qui est 10. Il s’affiche donc 10.

La deuxième invocation est faite par Call. Call transmet un nouveau scope qui est l’objet o. c’est dans ce scope que se fera la recherche de x qui vaut 15.

 

Il peut arriver que f attende des arguments. Il est possible de les lui passer.

La différence entre Call et Apply est que Call attend un tableau de paramètres tandis qu’Apply attend une liste. Examinons un exemple:

var x = 10;
var o = { x: 15 };
 
function f(message)
{
   alert(message);
   alert(this.x);
}
 
f("invoking f");
f.apply(o, ["invoking f through apply"]);

La méthode f a été modifiée pour recevoir un message que nous pouvons passer comme paramètre avec Apply ou Call.

 

revenons à notre module avec le code qui suit:

var mesModules=(
   function Manager() {
   var modules={};
 
   function define(name,dependencies,module) {
      for (var i=0;i<dependencies.length;i++)
      {
         dependencies[i]=modules[dependencies[i]];
      }
      modules[name]=module.apply(module,dependencies);
   }
 
   function get(name)
   {
      return modules[name];
   }
 
   return {
      define:define,
      get:get
   }
}
)();
 

 

On définit ainsi un gestionnaire de modules. Chaque fois que l’on a besoin de déclarer un nouveau module on le fera par la méthode define du gestionnaire. Par exemple pour définir un module appelé m1:

mesModules.define("m1", [], function() {
   function hello(who)  {
         return "Je vous présente: " + who;
      }
 
   return {
      hello:hello
   }
});

Le dernier paramètre est bien la déclaration d’un module comme nous l’avons vu précédemment. On pourrait consommer le module ainsi:

var m1=mesModules.get("m1");
 
console.log(m1.hello("Amethyste"));

Il s’affiche: Je vous présente Amethyste

 

Complétons le code de test avec un deuxième module m2 qui accepte un autre module comme dépendance:

mesModules.define("m2", ["m1"], function(m1) {
   var animal="hippo";
 
   function awsome(who)  {
         console.log(m1.hello(animal).toUpperCase());
      }
 
   return {
      awsome:awsome
      }
});

Le nouveau module est très simple, il expose une nouvelle méthode (awsome) qui se contente d’appeler la méthode hello du module m1 et de mettre en majuscule la sortie.

On testerai ainsi:

var m1=mesModules.get("m1");
var m2=mesModules.get("m2");
 
m2.awsome();

 

Browserify

Je vais terminer sur ce chapitre juste en signalant un outil: Browserify.

Je ne l’ai pas testé, mais en gros il permet de gérer les modules à la façon de NodeJS, c’est à dire en les chargeant depuis un fichier externe via une méthode require. Nous allons faire des choses de ce genre avec ES 6…

Que change ES 6?

 

Avec ES 6 plusieurs nouveautés sont apparues. Tout d’abord le mot clef let qui rend possible la déclaration d’un scope à l’intérieur de n’importe quel bloc d’accolades et pas juste les fonctions.

 
{ 
   let x=2; var y=3; console.log(x); 
} 
console.log(y);

Let s’emploie à la place du mot clef var. Si vous testez ce code sur ES6Fiddle vous constaterez bien que l’accolade fonctionne comme une définition de portée pour x.

Let ne supporte pas le hoisting toutefois, c’est bien dommage ou pas, à voir.

 

Vous vous souvenez de la discussion au sujet dus closures. on avait résolu un problème de closure en introduisant une IIFE. Let nous permet de simplifier la syntaxe:

for (var i=1;i<5;i++) {
   let j=i;
   setTimeout(function timer() {
      console.log(j);
   },1000);
 
}

Il est même possible d’aller un peu plus loin en déclarant let dans la boucle for elle-même:

for (let i=1;i<5;i++) {
   setTimeout(function timer() {
      console.log(i);
   },1000);
 
}

 

ES 6 apporte une nouvelle approche pour définir des modules à l’aide d’un fichier séparé. Deux nouveaux mots clefs sont définis

  1. export
  2. import

Vous trouverez ici un tuto très clair:

http://www.sitepoint.com/understanding-es6-modules/

 

Bibliographie

Outre les liens mentionnés dans l’article on peut aussi lire:

 

 

 

 

 

Publicités

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s