Usage avancé des fonctions JavaScript
Cet article est un complément à l’article sur les 3 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 :
[caption id=”attachment_897” align=”aligncenter” width=”542” caption=”javascript == la pornstar des langages de dev: souple, puissant, tu lui fait faire ce que tu veux, et ça peut finir bien crade.”][/caption]
Admettons donc que vous ayez digéré sans problème les portées et les fonctions, passons à deux choses vraiment particulières à JavaScript :
- 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,
- 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 n’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 passtaticMethod
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. Ca a l’air sale dit comme ça, mais nous allons voir un cas concret où cela est bien utile. Imaginez que vous construisez une mini-librairie 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ée. Si vous faîtes une librairie vous vous devez de ne pas ralentir les développeurs qui l’utiliseraient. Hors 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é, 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’objet 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 la 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 ! Hors onload
n’est appelé qu’après que la boucle se soit terminée, et i
a été incrémentée entretemps. Pour fixer cela, nous allons utiliser deux choses :
- l'auto-exécution, qui va nous permettre de copier la valeur de
i
- 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é à 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 info, ce cas d’école est souvent posée 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 :
- d'interdire la création directe d'un objet
- 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 );
};
})();
A ce stade nous avonc 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 unique 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é à é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 JS 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 librairies 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 post de blog ne suffira jamais pour comprendre des concepts de programmation : codez !