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 namespace utils. 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 prototypen’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 :

9 commentaires vers "Javascript Orienté Objet : syntaxe de base des classes JS à l’intention des développeurs PHP"

  1. 06/02/2010 - 17:56 | Lien permanent

    Pour ceux qui débute avec le javascript, effectivement il n’y a pas de documentation de référence parce que comme pour le HTML, le javascript est basé sur des normes et des RFC. Pour le reste effectivement c’est propre à chaque navigateurs. Toutefois il y a des sites relativement complet dans la documentation du DOM, notamment le site de la W3C et pour ceux qui préfère le français il y a aussi http://fr.selfhtml.org/ .

  2. 06/23/2010 - 20:24 | Lien permanent

    Salut, merci de m’avoir indiqué ton site dans les commentaires de mon blog, ça pourrait effectivement m’être utile :) .
    Je ne suis pas très fort en POO, mais ça a vraiment son importance.
    Je pensé à te donner mon adresse Email, si tu peux l’avoir sans que je la cite, alors ajoutes moi à tes contacts msn, si tu le veux bien, sinon si tu ne la vois pas, dis le ici en commentaire et je te la donnerais, j’aurais des trucs à te montrer que je fais en ce moment, assez sympa, et j’ai surtout un gros projet.
    Voilà j’attend ta réponse ici même, bonne continuation !

  3. 06/24/2010 - 12:08 | Lien permanent

    je vais avoir du mal à t’aider sur l’ensemble d’un projet :)
    mais si tu as quelques questions précises, on peut en discuter ici si c’est en rapport avec l’article, ou sur un forum (ceux d’alsacreations par exemple).
    Au moins tes questions et les réponses seront publiques, ce qui servira à d’autres ;)
    see you

  4. 07/01/2010 - 11:32 | Lien permanent

    Chuis sur le cul ! Pas une seule fois tu n’as écrit les pourtant primordiaux « check ton scope » ni « eval is evil » !! :p
    Blague à part, je pense qu’il manque juste un petit exemple d’instanceManager pour armer convenablement un débutant. :)

  5. 07/01/2010 - 12:21 | Lien permanent

    je ne voulais pas faire ce tuto plus long que nécessaire, mais je crois que tu as raison : je vais rajouter au moins le pattern Singleton

  6. 11/18/2010 - 14:34 | Lien permanent

    Merci ! Super guide, j’avais tendance à coder comme dans un fichier C et grâce à cet article, je viens de comprendre comment marche l’encapsulation des données en javascript.

    Je vais essayer de réécrire le javascript de mon site en respectant l’encapsulation quand j’aurai le temps :)

  7. 06/17/2012 - 07:01 | Lien permanent

    ça fait un moment que je m’amuse sur les divers moyens de faire du classique OO en js (fan du modèle objet de coffeescript et de backbone), et je trouve ton approche super bien expliquée, alors quelque chose sur l’héritage pour compléter ça serait sympa

  8. Unmecduweb's Gravatar Unmecduweb
    12/08/2012 - 12:10 | Lien permanent

    Parfait, je pouvais pas mieux tomber!
    Excellent boulot, les explication sont claires, les exemples bien choisi.
    Je reviendrai ;)

  9. sgtB's Gravatar sgtB
    12/15/2013 - 00:58 | Lien permanent

    C’est toujours d’actualité pour fin 2013 ? Le langage évolue et je suis débutant : m’armer d’un râteau à gazon rouillé en pensant que c’est un lance-roquettes m’embêterait, pour attaquer JS Objet.

Laisser un commentaire