Javascript Orienté Objet : syntaxe de base des classes JS à l’intention des développeurs PHP
Pourquoi, pour qui ?
Ce tuto a pour cible les développeurs qui ont une expérience du PHP (5) et qui veulent se lancer dans un projet Javascript qui dépasse le simple scripting. Cela va donc commencer par savoir écrire des classes en JS. Pour avoir galéré en tant que développeur puis en tant que lead technique avoir formé de bons développeurs PHP à faire des applications Web où le JS représente plus d’un tiers du code et la moitié du temps de dev, j’ai pu constater les énervements classiques lorsqu’on commence à vouloir faire des choses sérieuses en JS.
Le but ici n’est pas de rentrer dans la théorie du langage JS ou même d’être exhaustif (voir à la fin de cet article des ressources qui le font très bien) mais de vous fournir un template pour commencer à écrire vos classes.
JS 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 3 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é, le manque de documentation centralisée (pas d’équivalent à PHP.net) et enfin la version implémentée d’EcmaScript qui varie selon le browser (pour info, il faut en rester à la version 1.5 qui est celle de IE6-8)
Concrètement il y a 2 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, constructeur sont créées de cette seule manière
Architecture comparée JS/PHP
Eviter 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, l’arrivée d’un nouveau code ou toute modification de l’existant.
Exemple, ceci génère une boucle infinie :
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 font 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 JS 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.
for(var i= 0; ....
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
.
var MY_APP_NAMESPACE = {}; // l'équivalent namespace de PHP5.3 n'existe pas, on déclare un objet JS
MY_APP_NAMESPACE.genericFunctionName = function() {
var aMyArray = [ .... ], // multiples déclaration 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 si ils 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 JS 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, et dans des sous-namespaces en PHP5.3-JS.
Exemple d’une classe PHP servant à valider le format d’input utilisateur :
namespace MY_APP_NAMESPACE;
class validation {
public static $regMail='^[w-]+(?:.[w-]+)*@(?:[w-]+.)+[a-zA-Z]{2,7}$';
public static $regPassword='^[a-zA-Z0-9./\+=%ù£*^¨_&!@#-]{3,50}$';
static function isMailValid($sMail) {
return (preg_match("/".self::$regMail."/", $sMail) === 1) ;
}
static function isValidPassword($sPassword) {
return (preg_match("/".self::$regPassword."/", $sPassword ) === 1);
}
}
De n’importe où en PHP, on peut donc appeller ces fonctions de cette manière :
print MY_APP_NAMESPACEvalidation::isValidMail( 'mon@mail.com' );
Voici l’équivalent JS (Exécuter dans votre browser) :
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 JS 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 JS, on peut donc appeller ces fonctions de cette manière :
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écifités JS, 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 JS. Si vous aimez comprendre :
- La première et la dernière ligne est une fonction anonyme autoexécutée que l'on met autour de chaque classe. Elle sert à avoir un scope local propre à la classe et donc à émuler des variables privées pour cette classe.
- la 2nde ligne suppose que
MY_APP_NAMESPACE
a déjà été déclarée mais n'est pas certaine du sous namespaceutils
. La notation spéciale||
(double pipe) permet donc de créer ce sous-namespace uniquement si il n'est pas déjà déclaré (et donc de ne pas l'écraser) - la classe statique
validation
est concrètement un objet JS composé de 2 expressions régulières (directement compilées avec / /) et de 2 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 :
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 :
$date = new MY_APP_NAMESPACEcustomDate( 1268733478547 );
print $date->getMonthName(); // 'March'
Maintenant en JS (exécuter le code) :
(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 JS 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 (voirce petit bench, il en existe des dizaines d’autres qui confirment la même chose). A vous de faire la balance entre avoir des variables privées et de meilleures performances (exécuter le code) :
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 JS 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 qu’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 2 templates de classes de base (en mode prototype
et en mode closure
, le premier étant à préférer). Ces templates m’ont servi ces 4 dernières années sur des projets JS 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 JS Orienté Objet, vous êtes probablement sur un projet suffisamment large pour utiliser des librairies JS comme YUI ou jQuery qui ont chacun des méthodes pour simuler l’héritage (qui n’existe pas formellement en JS). Maintenant si ça vous intéresse, je peux faire un petit post là dessus, dites moi ça dans les commentaires.
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 :
- contre performance des variables privées, ou pourquoi préférer prototype
- A propos de la versatilité des function en JS (vidéo + slides + transcription 1h en anglais, par Douglas Crockford)
- une des premières explications concrètes du module pattern
- 5 manières de créer des objets, court et concis