JavaScript c'est compliqué

Quand on arrive du PHP, du C ou même de Java, JavaScript peut être franchement surprenant. Certains s'en amusent, d'autres prennent sa défense en rappelant son histoire mouvementée (la fusion de trois langages, une implémentation en quelques semaines, pris dans la Browser War depuis 15 ans) et surtout une chose qui est bien particulière aux développeurs Web : personne ne prend la peine de l'apprendre !

Ajouté à cela, il y a le DOM dont l'implémentation dans chaque browser varie, la programmation événementielle que les développeurs PHP n'ont en général jamais expérimentée, le manque de documentation centralisée (pas d'équivalent à PHP.net) et enfin la version implémentée d'ECMAScript qui varie selon le navigateur (pour info, il faut en rester à la version 1.5 qui est celle de IE6-8).

Concrètement, il y a deux choses à comprendre pour éviter les erreurs classiques et partir sur une bonne base de code pour programmer avec des objets :

  • le scope : repérer et utiliser var et les closures ou {} qui l'entourent, vos variables ne sont visibles qu'à l'intérieur ;
  • tout est function() : fonctions, méthodes, classes, constructeurs sont créés de cette seule manière.

Architecture comparée JavaScript/PHP

Éviter les globales, utiliser var et les namespaces

Valable dans les deux langages : vous allez essayer de minimiser le nombre de variables disponibles dans $_GLOBALS et window.* et donc modifiables par les librairies que vous incluez, par l'arrivée d'un nouveau code ou par toute modification de l'existant.

Exemple, ceci génère une boucle infinie :

 
Sélectionnez

function genericFunctionName() {
for(i = 0; i < myArray.length; i++) {
	....
}

for(i = 0; i < 10; i++) {
	genericFunctionName();
}

On a créé ici une boucle infinie car le i à l'intérieur et à l'extérieur de la fonction fait référence à window.i : c'est la même variable globale. Ici la première librairie venue (ou publicité) risque en outre d'écraser le nom de votre fonction si il est trop générique : j'ai déjà vu par exemple jQuery et l'API Mappy se gêner l'un l'autre sur la même page ! Et, même si JavaScript est monothread, il y a des cas où la valeur de i peut être modifiée par une autre boucle qui utilise ce nom si répandu.

La solution ici est de rajouter var pour que le i à l'intérieur de la fonction ne soit pas visible de l'extérieur. Son scope est la closure la plus proche, c'est-à-dire la déclaration de fonction la plus proche.

 
Sélectionnez

for(var i= 0; i < myArray.length; i++)

Pour partir sur de bonnes bases, on va créer un namespace pour tout le code de notre application Web. Pendant tous les développements, il faudra prendre l'habitude de déclarer ses variables avec var.

 
Sélectionnez

var MY_APP_NAMESPACE = {}; // l'équivalent namespace de PHP5.3 n'existe pas, on déclare un objet JavaScript

MY_APP_NAMESPACE.genericFunctionName = function() {
	var aMyArray = [ .... ], // multiples déclarations de variables locales
	iTotal = aMyArray.length;
	for(var i = 0; i < iTotal; i++) ....
}; // notez le ; final, on a tendance à l'oublier

for(i = 0; i < 10; i++) {
	MY_APP_NAMESPACE.genericFunctionName();
}

La boucle dans la fonction est protégée, ni i ni le tableau ne sont accessibles de l'extérieur. Il est peu probable que des codes extérieurs utilisent votre namespace et s'ls le font, vous le sentirez de toute manière passer, ce qui (sans ironie) est plus facile à détecter qu'un petit bug !

Classes statiques

Toujours dans l'idée de libérer notre espace global, c'est une bonne pratique en PHP comme en JavaScript d'arrêter d'accumuler des listes sans fin de fonctions : il vaut mieux les regrouper par thème dans des « classes statiques » en PHP5, dans des sous-namespaces en PHP5.3-JavaScript.

Exemple d'une classe PHP servant à valider le format d'input utilisateur :

 
Sélectionnez

namespace MY_APP_NAMESPACE;
class validation {
	public static $regPassword='^[a-zA-Z0-9.\/\\+=%ù£*^¨_&!@#-]{3,50}$';

	static function isValidMail($sMail) {
		return (filter_var($sMail, FILTER_VALIDATE_EMAIL));
	}
	static function isValidPassword($sPassword) {
		return (preg_match("/".self::$regPassword."/", $sPassword ) === 1);
	}
}

De n'importe où, en PHP, on peut donc appeler ces fonctions de cette manière :

 
Sélectionnez

print MY_APP_NAMESPACE\validation::isValidMail( 'mon@mail.com' );

Voici l'équivalent JavaScript (Exécuter dans votre navigateur) :

 
Sélectionnez

MY_APP_NAMESPACE = {};

(function(){ // début de scope local
MY_APP_NAMESPACE.utils = MY_APP_NAMESPACE.utils || {};
// déclaration de la classe de validation proprement dite
MY_APP_NAMESPACE.utils.validation = {
    // déclaration de nos variables statiques
    regMail: /^[\w-]+(?:\.[\w-]+)*@(?:[\w-]+\.)+[a-zA-Z]{2,7}/,
    regPassword: /^[a-zA-Z0-9.\/\\+=%ù£*^¨_&!@#-]{3,50}/,
    // déclaration de nos méthodes
    isMailValid:function( sMail ) {
        return (self.regMail.exec( sMail ) != null);
    },
    isValidPassword:function( sPassword ) {
        return (self.regPassword.exec( sPassword ) != null);
    }
}; // fin de classe

// trick JavaScript pour émuler le self:: en PHP : on utilise une variable locale
var self = MY_APP_NAMESPACE.utils.validation;
})(); // fin de scope local

De n'importe où, en JavaScript, on peut donc appeler ces fonctions de cette manière :

 
Sélectionnez

alert(MY_APP_NAMESPACE.utils.validation.isMailValid( 'monm@il.com' )); //true
alert(MY_APP_NAMESPACE.utils.validation.isMailValid( 'moççna@il.com' )); // false

La syntaxe est radicalement différente de PHP car il n'y a rien d'explicite et on exploite plusieurs spécificités JavaScript, mais on est arrivé au même résultat. Je vous conseille de prendre ce bout de code comme un template pour créer des classes statiques JavaScript.
Si vous aimez comprendre :

  • la première et la dernière ligne sont des fonctions anonymes auto-exécutées que l'on met autour de chaque classe. Elles servent à avoir un scope local propre à la classe et donc à émuler des variables privées pour cette classe ;
  • la 2de ligne suppose que MY_APP_NAMESPACE a déjà été déclarée mais n'est pas certaine du sous namespace utils. La notation spéciale || (double pipe) permet donc de créer ce sous-namespace uniquement s'il n'est pas déjà déclaré (et donc de ne pas l'écraser) ;
  • la classe statique validation est concrètement un objet JavaScript composé de deux expressions régulières (directement compilées avec / /) et de deux fonctions. Le tout est déclaré avec la notation JSON : { clé : valeur , … }. Attention à ne jamais laisser trainer une virgule derrière la dernière clé, car cela fait planter IE et il ne le dit pas clairement ;
  • dans l'avant-dernière ligne, on déclare une variable privée qu'on appelle self et qui remplit la même fonction qu'en PHP : faire référence à notre classe statique. Comme en PHP elle est là pour des raisons de confort (éviter de retaper entièrement et en dur le nom de la classe).

Objets instanciables

Créons une classe PHP classique avec constructeur, méthode publique, variable privée et variable statique publique. Prenons par exemple un objet permettant de manipuler des dates :

 
Sélectionnez

namespace MY_APP_NAMESPACE;
class customDate {
	private $iTimestamp = 0; // variable privée propre à chaque instance
	static $aMonthNames = array('January', ....); // variable statique partagée pour tout le code
	// constructeur
	public function __construct($iTimestamp) {
		$this->iTimestamp = $iTimestamp;
	}
	// méthode publique propre à chaque instance
	public function getMonthName() {
		$month_number = date('n', $this->iTimestamp) -1;
		return self::$aMonthNames[$month_number];
	}
}

Utilisation :

 
Sélectionnez

$date = new MY_APP_NAMESPACE\customDate( 1268733478547 );
print $date->getMonthName(); // 'March'

Maintenant en JavaScript (exécuter le code) :

 
Sélectionnez

(function(){ // début de scope local
MY_APP_NAMESPACE.utils = MY_APP_NAMESPACE.utils || {}; // création d'un sous namespace pour y stocker nos classes utilitaires si celui-ci n'est pas déjà créé

// constructeur
MY_APP_NAMESPACE.utils.customDate = function( iTimeStamp ) {
	this.iTimeStamp = iTimeStamp;
	this.date = new Date();
	this.date.setTime(this.iTimeStamp);
};

// variables et méthodes publiques propres à chaque instance
MY_APP_NAMESPACE.utils.customDate.prototype = {
	date:null,
	iTimeStamp:0,
	getMonthName:function() {
		var iMonthNumber = this.date.getMonth();
	return self.aMonthNames[iMonthNumber];
	}
};

// variable statique partagée pour tout le code
MY_APP_NAMESPACE.utils.customDate.aMonthNames = ['January', 'February','March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];

// trick JavaScript pour émuler le self:: en PHP : on utilise une variable locale
var self = MY_APP_NAMESPACE.utils.customDate,
privateVariable = 0; // variable privée visible par toutes les instances

})(); // fin de scope local

Remarquez que cette syntaxe (dite prototype) n'autorise pas à avoir des variables privées pour chaque instance comme en PHP: ici this.iTimeStamp se réfère bien à l'instance de customDate, mais est accessible depuis l'extérieur. La variable vraiment privée est privateVariable mais elle est visible et modifiable par toutes les instances, concept qui serait équivalent en PHP à une variable statique privée, qui peut par exemple servir à un manager d'instance (pattern factory + accessor) pour stocker une liste des instances en cours.

Syntaxe alternative

Il existe une autre syntaxe, dite closure, qui permet d'avoir des variables et méthodes privées pour chaque instance mais elle est moins performante si vous instanciez des centaines d'objets, ou que vos objets commencent à contenir plusieurs méthodes (voir ce petit bench, il en existe des dizaines d'autres qui confirment la même chose). À vous de faire la balance entre avoir des variables privées et de meilleures performances (exécuter le code) :

 
Sélectionnez

MY_APP_NAMESPACE = {};

(function(){ // début de scope local
MY_APP_NAMESPACE.utils = MY_APP_NAMESPACE.utils || {}; // création d'un sous namespace pour y stocker nos classes utilitaires si celui-ci n'est pas déjà créé

// constructeur
MY_APP_NAMESPACE.utils.customDate = function( iTimeStamp ) {
    // ces variables resteront privées, spécifiques à l'instance
    var iTimeStamp = iTimeStamp,
        date = new Date();
    date.setTime(iTimeStamp);

    // on renvoie ce qui est public sous la forme d'un objet
    return {
        getMonthName:function() {
            var iMonthNumber = date.getMonth();

            return self.aMonthNames[iMonthNumber];
        }
    };
};

// variable statique partagée pour tout le code
MY_APP_NAMESPACE.utils.customDate.aMonthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];

// trick JavaScript pour émuler le self:: en PHP : on utilise une variable locale
var     self = MY_APP_NAMESPACE.utils.customDate,
    privateVariable = 0; // variable privée visible par toutes les instances
})(); // fin de scope local

// privateVariable est privé
console.log('variable privée :'+ typeof privateVariable ); // undefined
// création d'un objet date
var date1 = new MY_APP_NAMESPACE.utils.customDate(1268733478547); // 16 mars 2010, 10:58
console.log('méthode publique d instance :'+date1.getMonthName()); // March
console.log('variable privée d instance :'+ typeof date1.date ); // undefined

// la variable statique est disponible en lecture
console.log('variable statique : '+MY_APP_NAMESPACE.utils.customDate.aMonthNames[0]); // January

// et aussi en écriture, ici on change de langue à la volée
MY_APP_NAMESPACE.utils.customDate.aMonthNames = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Decembre'];
console.log( date1.getMonthName() ); // Mars

Ici on a remplacé l'utilisation de prototype pour ajouter des propriétés par un return dans le constructeur d'un objet contenant des propriétés publiques. Comme on se trouve dans le scope du constructeur, les méthodes ont accès à iTimestamp et date qui sont privées et propres à chaque instance. Le coût en performances vient du fait que les fonctions sont redéfinies à chaque fois que l'on crée un objet, alors qu'avec prototype, la définition n'était faite qu'une seule fois.

Conclusion

Vous voici donc avec la notion salutaire de namespace, une implémentation de self::, une idée sur le fonctionnement du scope et deux templates de classes de base (en mode prototype et en mode closure, le premier étant à préférer). Ces templates m'ont servi ces quatre dernières années sur des projets JavaScript d'envergure moyenne (+ de 100 classes, des milliers de lignes de code), ce modèle est donc éprouvé.

Notez que je n'ai pas traité l'héritage des classes, c'est parce que je suppose que si vous en êtes à vouloir développer en JavaScript Orienté Objet, vous êtes probablement sur un projet suffisamment large pour utiliser des librairies JavaScript comme YUI ou jQuery qui ont chacune des méthodes pour simuler l'héritage (qui n'existe pas formellement en JavaScript). Maintenant si ça vous intéresse, je peux faire un petit post là-dessus, dites-moi ça dans les commentaires.

Références et remerciements

JavaScript étant extrêmement versatile, sachez cependant qu'il existe une bonne dizaine de variations autour des closures et de prototype. Le tout étant d'en choisir une qui marche dans tous les cas (et de savoir pourquoi), voici une short-list de références :

L'article original sur le blog BrainCracking.

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