Usage avancé des fonctions JavaScript

Jean-Pierre Vincent

Cet article est un complément à l'article sur les trois fondamentaux de JavaScript, il vaut mieux être déjà à l'aise avec JavaScript avant de crier au scandale en voyant ce qu'on peut en faire. Pour reprendre un bon mot de quelqu'un qui avait assisté à ma conférence sur JavaScript.

Commentez

- Titre : Usage avancé des fonctions JavaScript

- Auteur : Jean-Pierre Vincent

- Parution : 5 décembre 2011

- Licence : Copyright ® 2011 Jean-Pierre Vincent. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- Introduction

- Classe statique

- Renvoi de fonction

- Implémenter des design pattern

- Conclusion

- Références et remerciements

Introduction

JavaScript == la pornostar des langages de développement : souple, puissant, tu lui fais faire ce que tu veux et ça peut finir bien crade.

Admettons donc que vous ayez digéré sans problème les portées et les fonctions, passons à deux choses vraiment particulières à JavaScript :

1. Le renvoi de fonction qui permet de belles optimisations et qui ouvre la voie à des patterns que les amoureux de la théorie du langage apprécieront ;

2. Une implémentation de classe statique, pour reprendre le terme utilisé en PHP ou en Java.

Et enfin nous verrons une proposition d'implémentation de deux design pattern célèbres et particulièrement utiles en JavaScript : Singleton et Factory.

Classe statique

Pour rappel, en PHP et dans d'autres langages, une propriété ou une méthode statique peut être appelée sans que la classe ait été instanciée pour créer un objet. C'est généralement là que l'on range les constantes ou les fonctions utilitaires par exemple. En JavaScript, tout étant objet y compris les fonctions, cela se fait assez naturellement :

// constructeur
var myClass = function () {
};
myClass.staticMethod = function() {
console.log('OK');
};
// que voit-on au niveau global ?
myClass.staticMethod(); // OK

Regardez la manière dont est définie staticMethod : on la range directement dans la fonction myClass ! Elle est donc directement disponible sans passer par la création d'un objet. Comparons d'ailleurs avec une définition de méthode d'objet comme on l'a vu dans les paragraphes précédents pour bien comprendre où sont disponibles ces nouvelles méthodes de classe.

// constructeur
var myClass = function () {
return {
publicMethod:function() {
console.log('OK');
}
}
};
myClass.staticMethod = function() {
console.log('OK');
};

// que voit-on au niveau global ?
myClass.publicMethod(); // Error
myClass.staticMethod(); // OK

// que voit l'instance ?
myObject = myClass();
myObject.publicMethod(); // OK
myObject.staticMethod(); // Error

Si vous exécutez ce code dans votre console, vous allez voir où se produisent les erreurs :

- vous ne pouvez pas accéder à publicMethod sans avoir d'abord instancié myClass ;

- l'instance de myClass ne contient pas staticMethod car celle-ci est directement disponible à partir du nom de la classe.

Renvoi de fonction

Une fonction peut se redéfinir elle-même quand elle s'exécute. Ça a l'air sale dit comme ça, mais nous allons voir un cas concret où cela est bien utile. Imaginez que vous construisez une minibibliothèque dont une des fonctions permet de se rattacher à un événement du DOM. Pour supporter tous les navigateurs, il y a deux méthodes aux noms distincts et aux arguments légèrement différents que vous pouvez encapsuler dans une fonction en faisant un simple if.

var onDOMEvent =
function( el, ev, callback) {
// le monde de Microsoft
if(document.body.attachEvent){
el.attachEvent('on'+ev, callback);
// le monde du W3C
} else {
el.addEventListener( ev, callback, false);
}
};

Cette fonction marche très bien, mais à chaque fois qu'elle est exécutée, le test sur la condition aussi est exécuté. Si vous faites une bibliothèque, vous vous devez de ne pas ralentir les développeurs qui l'utiliseraient. Or cette fonction pourrait très bien être appelée des centaines de fois, exécutant ainsi inutilement du code. Pour optimiser cela, nous allons redéfinir la fonction à la volée lors de sa première exécution.

var onDOMEvent =
function( ) {
if(document.body.attachEvent) {
return function(element, event, callback) {
element.attachEvent('on'+ event, callback);
};
} else {
return function(element, event, callback) {
element.addEventListener( event, callback);
};
}
}();

Comme vous le voyez :

- cette fonction est auto-exécutée grâce aux deux parenthèses finales et n'a plus besoin des arguments puisqu'elle ne sera plus jamais exécutée après ;

- le if reste, mais il ne sera exécuté qu'une seule fois ;

- la fonction renvoie des fonctions anonymes contenant les codes spécifiques au navigateur, notez que ces fonctions attendent toujours les mêmes paramètres ;

- lorsque onDOMEvent() sera appelée, seul l'un ou l'autre corps de fonction sera exécuté, nous avons atteint notre objectif.

C'est une technique d'optimisation pas forcément évidente à intégrer mais qui donne de très bons résultats. Cela irait un peu trop loin pour cet article, mais si vous avez l'âme mathématique, cherchez donc sur le Web comment calculer la suite de Fibonacci en JavaScript, avec et sans « memoization » (Wikipedia). Vous pouvez également créer des fonctions spécialisées qui capturent certains paramètres pour vous éviter d'avoir à les préciser à chaque fois, technique connue sous le nom de currying (voir ce post de John Resig à ce sujet).

Autre cas concret d'école d'utilisation de cette technique. Partons du code suivant qui boucle sur un petit tableau d'objets et qui rattache l'événement onload à une fonction anonyme.

var queries = [ new XHR('url1'), new XHR('url2'), new XHR('url3')];
for(var i = 0; i < queries.length; i++) {
queries[i].onload = function() {
console.log( i ); // référence
}
}

Observez bien la valeur de i : notre fonction anonyme crée une portée, le parseur JavaScript ne voit pas i dans cette fonction, il remonte donc d'un niveau pour le trouver. Jusqu'ici tout va bien notre variable est bien référencée. Pourtant lorsque l'événement onload est appelé par le navigateur, nous avons une petite surprise :

queries[ 0 ].onload(); // 3!
queries[ 1 ].onload(); // 3!
queries[ 2 ].onload(); // 3!

L'interpréteur JavaScript a correctement fait son boulot : la fonction onload voit bien i (sinon nous aurions eu undefined), mais c'est une référence vers la variable, pas une copie de sa valeur ! Or onload n'est appelé qu'après que la boucle s'est terminée et i a été incrémentée entre-temps. Pour fixer cela, nous allons utiliser deux choses :

1. L'auto-exécution, qui va nous permettre de copier la valeur de i ;

2. Le renvoi de fonction pour que onload soit toujours une fonction.

Attention, ça peut piquer les yeux :

for(var i = 0; i < queries.length; i++) {
queries[i].onload =  function(i) {
return function() {
console.log( i ); // valeur
};
}(i); // exécution immédiate
}
// plus tard ...
queries[ 0 ].onload(); // 0
queries[ 1 ].onload(); // 1
queries[ 2 ].onload(); // 2

Essayez de suivre le chemin de l'interpréteur :

- i est donnée à la fonction anonyme auto-exécutante ;

- le paramètre de cette première fonction anonyme s'appelle aussi i : dans cette portée locale, i a pour valeur 0 (pour la première passe) ;

- la fonction renvoyée embarque toute la portée avec elle et n'a donc que la valeur de ce nouveau i, qui ne bougera plus.

Pour information, ce cas d'école est souvent posé lors des entretiens d'embauche si on veut vérifier que vous avez bien compris les portées.

Implémenter des design pattern

En combinant namespace (voir l'article JavaScript pour les développeurs PHP), portée maîtrisée, espace privé et émulation d'objets, nous allons implémenter le design pattern Factory. Factory ou Singleton sont très intéressants en JavaScript, notamment pour les Widgets (type jQuery UI) : vous pouvez vous retrouver sur des pages où vous ne savez pas si le JavaScript d'un Widget s'est déjà exécuté sur tel élément du DOM. Pour optimiser, vous ne voulez pas recréer systématiquement le Widget, mais plutôt créer une nouvelle instance ou récupérer l'instance en cours. Vous avez donc besoin :

1. D'interdire la création directe d'un objet ;

2. De passer par une fonction qui va faire la vérification pour vous et vous renvoyer une instance.

Commençons par créer notre espace de travail, ainsi que notre namespace :

(function(){
// création ou récupération du namespace et du sous-namespace
MY = window.MY || {};
MY.utils = MY.utils || {};
// constructeur
MY.utils.XHR=function( url ){
console.log( url );
};
})();

À ce stade, nous avons une classe accessible de l'extérieur avec new MY.utils.XHR( url );. C'est bien mais pas top, nous voudrions passer par une fonction qui va vérifier s'il n'existe pas déjà une instance avec ce même paramètre URL. Nous allons déclencher une erreur en cas d'appel direct (comme en PHP) et prévoir une fonction getInstance( url ) qui va se rattacher au namespace de la fonction constructeur.

(function(){
// création ou récupération du namespace et du sous-namespace
MY = window.MY || {};
MY.utils = MY.utils || {};
// constructeur
MY.utils.XHR=function( url ){
throw new Error('please use MY.utils.XHR.getInstance()');
};
// factory
MY.utils.XHR.getInstance = function( url ) {
};
})();

Enfin, nous allons introduire une variable privée qui va contenir la liste de nos instances (un objet currentInstances avec en index l'URL et en valeur l'instance). Nous allons également rendre privé notre vrai constructeur.

(function(){
// constructeur
MY.utils.XHR=function( url ){
throw new Error('please use MY.utils.XHR.getInstance()');
};

//constructeur privé
var XHR = function( url ){
console.log( url );
};
// liste privée d'instances
var currentInstances = {};

// factory
MY.utils.XHR.getInstance = function( url ) {
// déjà créé ? on renvoie l'instance
if(currentInstances[url]) {
return currentInstances[url];
// on crée, on enregistre, on renvoie
} else {
return currentInstances[url] = new XHR(url);
}
};
})();

Telle quelle, cette implémentation permet déjà de créer une Factory pour des objets qui ont besoin d'être uniques mais qui n'ont qu'un seul paramètre (id d'un objet DOM, URL…). La vraie raison d'être d'une Factory, c'est de gérer des objets complexes à instancier, à vous donc d'étendre ses fonctionnalités. Les puristes auront remarqué que l'objet renvoyé n'était pas du type MY.utils.XHR et qu'on ne pouvait donc pas faire de vérification avec instanceof du type de l'objet. Honnêtement je ne connais pas de bon moyen de le faire, à vous de voir si c'est un manque dans votre code.

Vous voilà parés pour écrire du code un peu plus maintenable et mieux rangé et vous pourrez crâner dans les dîners en ville en racontant vos exploits de codeur JavaScript orienté objet.

Conclusion

- JavaScript a des concepts différents des langages majeurs et devient extrêmement important sur votre CV. Prenez le temps de l'apprendre.

- Les bibliothèques telles que jQuery ne sont pas faites pour couvrir les cas que nous venons de voir. jQuery permet d'utiliser le DOM sereinement, pas d'organiser votre code proprement.

- J'ai essayé d'être pratique, mais lire un article ne suffira jamais pour comprendre des concepts de programmation : codez !

Références et remerciements

L'article original peut être lu sur le site BrainCracking : Usage avancé des fonctions JavaScript.

Je tiens à remercier ClaudeLELOUP et jacques_jean pour leur relecture attentive de cet article.