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 :

javascript == la pornstar des langages de dev: souple, puissant, tu lui fait 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 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 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. 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 :

  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é à 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 :

  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 );
};
})();

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 !

7 commentaires vers "Usage avancé des fonctions JavaScript"

  1. 11/17/2011 - 15:39 | Lien permanent

    j’ai pas bien compris ton 2eme exemple sur le renvoi de fonction.
    Je comprends bien l’éxécution immédiate mais dans l’exemple tu veux que la methode soit déclenché sur l’évement onload. Or a cause de (i) la function est éxecutée tout de suite et pas « onload » comme il est attendu…

  2. 11/18/2011 - 09:03 | Lien permanent

    haaaaa j’ai lu beaucoup trop vite. J’avais ecris ca dans mon browser:

    query[i].onload = function() {
    console.log( i ); // référence
    }(i);

    Je ne comprenais pas l’avantage d’éxecuter tout de suite. Maintenant je vois, il y a deux function.

    Merci

  3. FredB's Gravatar FredB
    04/08/2012 - 18:29 | Lien permanent

    Excellent article.
    Trop peu de développeurs utilisent Javascript correctement (ce qui n’est pas si facile)
    Recommandé à tout mon entourage. Encore Bravo :-)

  4. charles's Gravatar charles
    05/22/2012 - 00:08 | Lien permanent

    Merci encore pour ton article vraiment intéressant, je viens de réutiliser le renvoi de fonction avec exécution immédiate.

  5. marco_105's Gravatar marco_105
    07/18/2012 - 22:40 | Lien permanent

    Merci de partager tes connaissances pointues sur le sujet, ça ouvre des perspectives d’approche de ce langage qui est par ailleurs plein de subtilités et demande une réflexion toute nouvelle en matière de POO.

  6. Hamadi's Gravatar Hamadi
    03/22/2013 - 23:51 | Lien permanent

    Merci pour l’article :-)
    Cependant je ne comprend pas bien cette volonté récurrente de vouloir faire du javascript un langage POO. En effet,il n’en est simplement pas un !
    Pour les design patterns le pattern factory n’existe pas ,seulement factory method ou abstract factory (et c’est ici ni l’un ni l’autre).Et pour le singleton,il n’a pas d’autre vocation que de retourner la même instance, pas de gérer une collection d’instances !
    ce pattern est en réalité un multiton.Ceci dit bravo pour le travail et bonne continuation

Laisser un commentaire

4 Trackbacks vers "Usage avancé des fonctions JavaScript"

  1. sur 11/16/2011 at 23:34
  2. sur 11/19/2011 at 17:09
  3. sur 02/08/2012 at 11:24
  4. sur 10/30/2012 at 17:35