La programmation orientée objet en JavaScript pour les nuls

Logo JavaScriptLa programmation orientée objet en JavaScript est un sujet délicat parfois, la documentation est souvent vague et il n’y a pas de très bonnes ressources en Français. Ainsi en vue de mes prochains articles, j’ai pensé qu’un cours de POO en JavaScript en partant de zéro ne ferait pas de mal. Je ne me considère pas comme un barbu en JS, mais je le pratique assez chaque jour sur des projets concrets pour savoir l’utiliser et l’apprécier à sa juste valeur 😉 On va commencer avec les bases en créant une une classe avec des membres publiques, puis privés, puis statiques. Ensuite nous verrons l’héritage !

1. Une classes simple

Dans ce premier exemple, nous allons créer une classe Sprite que l’on retrouve souvent lors de la création d’un jeu. J’ai choisi de faire la comparaison avec le langage Java pour deux raisons. Déjà le Java c’est trop cool =D La première est que les débutants pensent quelque fois que Java et JavaScript c’est la même chose, c’est totalement faux. Enfin le langage Java est facile à écrire et permettra de mettre en situation rapidement les différents cas sans avoir recourt à des mots clé hasardeux.

Sprite.java

public class Sprite {
	protected float _x;
	protected float _y;
	protected String _name;

	public Sprite(String name, float x, float y) {
		_name = name;
		_x = x;
		_y = y;
	}

	public String toString() {
		return _name + " { x: " + _x + " y: " + _y + " }";
	}

	public static void main(String args[]) {
		Sprite sprite = new Sprite("player", 15.0f, 50.0f);
		System.out.println(sprite.toString());
	}
}

Cette classe permet de représenter un sprite à l’écran, elle possède trois informations que l’on stock dans des variables protégées. La méthode toString() quant-à elle va permettre d’afficher une trace du sprite avec sa position (utile pour le debug par exemple). Enfin la méthode main, obligatoire en Java, sera chargée de créer une instance de la classe et d’appeler la méthode toString(). Facile ? Bon passons à la version JavaScript maintenant !

Sprite.js

var Sprite = function (name, x, y) {
	var _name = name,
		_x = x,
		_y = y;

	this.toString = function () {
		return _name + " { x: " + _x + " y: " + _y + " }";
	};
};

var sprite = new Sprite("player", 15.0, 50.0);
console.log(sprite.toString());

Alors à vue d’œil comme ça je dirais qu’il y a moins de code pas vous ? En JavaScript on déclare une classe comme une fonction et cette dernière sert d’ailleurs de constructeur. Ainsi quand on écrit var MaClass = function (p1, p2) {} on déclare la classe et son constructeur en même temps. Au final ce n’est pas si mal car on a moins de code à écrire. Vous remarquerez que j’ai ajouté une fonction toString() préfixé par this dans le constructeur. Quand vous êtes dans le constructeur, vous êtes aussi dans l’objet en lui même, c’est donc ici que vous pourrez déclarer des fonctions privés ou publiques. Ce qu’il faut savoir c’est qu’une variable ou fonction déclarée avec le mot clé var sera privée et accessible uniquement dans le scope courant (scope == portée de la fonction), alors qu’une variable ou une fonction déclarée avec le mot clé this sera publique et accessible depuis l’intérieur et l’extérieur de la classe. J’attire votre attention sur le fait qu’il n’existe pas d’équivalent au mot clé protected. Enfin dernier point qui me tiens très coeur, n’oubliez pas ces foutues virgules, dés que vous utilisez le mot clé var, alors vous déclarez une variable (qui contient une fonction, un objet, un nombre) et après la déclaration d’une variable on met quoi ? Un point virgule. Cette rigueur vous permettra d’éviter quelques insultes de la part des compresseurs JavaScript.

2. Des classes un peu plus complexes

Nous avons vue comment créer une classe en JavaScript, c’est bien, cependant la méthode que j’ai montré a des limites. A chaque fois que vous aller créer une instance de cette classe, la méthode toString() sera elle aussi recréée. Si vous n’avez pas beaucoup d’objet et que ces derniers ne contiennent pas trop de méthodes, alors c’est négligeable. Cependant si votre classe contient une méthode qui est appelée très régulièrement par plusieurs instances alors c’est plus problématique car on consomme de la mémoire pour rien. Pour résoudre ce problème on utilise… les prototypes !

Si vous avez fait du C++ alors sachez que ça y ressemble beaucoup. On va donc créer la classe avec ses attributs, puis déclarer en dehors, des fonctions spéciales qui elles ne seront créées qu’une seule fois, même si vous avez plusieurs instances de l’objet. La petite subtilité est que ces méthodes prototype ne se trouvent pas dans le scope du constructeur puisqu’elles sont déclarées en dehors, ainsi vous devrez rendre vos attributs publiques pour pouvoir y accéder. Comment ça je suis dingue ? On passe tout en publique ? Mais qu’est-ce qu’on fait des principes même de la programmation orientée objet avec l’encapsulation des données ? Du calme les amis, on va préfixer nos variables avec un underscore, c’est uniquement symbolique, mais ça servira à dire à la personne qui utilisera votre classe « Touche pas à ça, c’est privé OK ? ». D’un autre côté les éditeurs de code qui proposent l’auto-completion, n’affichent pas les variables ou méthodes avec un underscore devant 😉

Sprite.js version prototype

var Sprite = function (name, x, y) {
	this._name = name;
	this._x = x;
	this._y = y;
};

Sprite.prototype.toString = function () {
	return this._name + " { x: " + this._x + " y: " + this._y + " }";
};

Au fait, rien ne vous empêche de mixer les deux méthodes, avec des fonctions dans le constructeur et des fonctions prototypes. Bon tout ça est pas mal, vous savez comment faire de la POO de base, maintenant voyons l’héritage car cette partie est importante.

3. L’héritage

La classe Sprite est très souvent dérivée pour créer des entités bien spécifiques comme un joueur, un ennemi, un objet d’inventaire, etc.. J’ai légèrement modifié l’implémentation de cette dernière pour lui ajouter des méthodes initialize(), update() et draw(). Notez que les objets SpriteBatch et Screen sont fictifs et uniquement dans ce code pour « habiller » la démonstration.

Sprite.java

public class Sprite {
	protected float _x;
	protected float _y;
	protected String _name;

	public Sprite(String name, float x, float y) {
		_name = name;
		_x = x;
		_y = y;
	}

	public void initialize() {
		_x = (_x < 0 || _x > Screen.width) ? _x = Screen.width / 2 : _x;
		_y = (_y < 0 || _y > Screen.height) ? _y = Screen.height / 2 : _y;
	}

	// Méthode appelée à chaque frame.
	public void update(GameTime gameTime) {
		// Test de collision bidon avec les murs
		_y = (_y < 0) ? 0 : _y; 		_y = (_y > Screen.width) ? Screen.width : _y;
		// Etc...
	}

	public void draw(SpriteBatch spriteBatch) {
		System.out.println("Dessin du sprite");
	}

	public static void main(String args[]) {
		Sprite sprite = new Sprite("player", 15.0f, 50.0f);
		sprite.draw();
	}
}

Player.java

public class Player extends Sprite {
	private String _job;

	public Player(String name, float x, float y, String job) {
		super(name, x, y);
		_job = job;
	}

	public void draw(SpriteBatch spriteBatch) {
		super.draw(spriteBatch);
		System.out.println("Je dessine en plus une icone du job");
	}
}

La classe Player dérive de Sprite et ajoute un attribut en plus, le job du personnage. La méthode draw() est quant-à elle surchargée, elle doit appeler celle de la classe mère, mais en plus elle doit dessiner l’icone du métier du joueur. Passons maintenant à l’implémentation en JavaScript qui va être assez différente, et hélas va nécessiter un peu de code pas facile à retenir la première fois. Il y a plusieurs manières de proposer un héritage en JavaScript, je vous propose deux approches qui fonctionnent toutes les deux, cependant je vous encourage vivement à utiliser la 2éme méthode qui n’est certes pas compatible avec les vieux navigateurs, mais qui pose beaucoup moins de problèmes.

Sprite.js

var Sprite = function (name, x, y) {
	this._name = name;
	this._x = x;
	this._y = y;
};

Sprite.prototype.initialize = function () {
	this._x = (this._x < 0 || this._x > Screen.width) ? this._x = Screen.width / 2 : this._x;
	this._y = (this._y < 0 || this._y > Screen.height) ? this._y = Screen.height / 2 : this._y;
};

// Méthode appelée à chaque frame.
Sprite.prototype.update = function (gameTime) {
	// Test de collision bidon avec les murs
	this._y = (this._y < 0) ? 0 : this._y; 	this._y = (this._y > Screen.width) ? Screen.width : this._y;
	// Etc...
};

Sprite.prototype.draw = function (spriteBatch) {
	console.log("Dessin du sprite");
};

var sprite = new Sprite("Player", 15, 50);
console.log(sprite.draw());

La grosse différence avec la classe Java est qu’on rajoute des this un peu partout car sans ça on ne peux pas exploiter les membres de la classe. Autrement le code métier est identique.

Player.js

var Player = function (name, x, y, job) {
	Sprite.call(this, name, x, y);
	this._job = job;
};

Player.prototype = new Sprite();

Player.prototype.draw = function (spriteBatch) {
	Sprite.prototype.draw.call(this, spriteBatch);
	console.log("Je dessine en plus une icone du job");
};

var player = new Player ("player", 15.0, 50.0, "Knight");
console.log(player.draw());

Ha ha ha ! Alors là c’est fini le « c’est facile » hein 😉 Je vous explique car ce n’est pas compliqué. L’héritage en JavaScript se déroule en deux étapes. La première consiste à appeler le constructeur de la classe mère dans le constructeur de la classe héritée. C’est en quelque sorte le super() de Java, à l’exception qu’il permet de dire que l’on dérive de la classe mère et que l’on appel en plus son constructeur. La syntaxe pour est la suivante :

var ClassFille = function (p1, p2) {
    ClassMere.call(this, p1, p2);
}

La fonction call prendra en premier paramètre l’instance de la classe fille, à savoir this, ensuite il faudra passer les paramètres à la classe mère.

Ce n’est pas la seule subtilité car en écrivant ça, vous récupérez les attributs publiques de la classe mère dans la classe fille, donc variables et méthodes déclarées avec le mot clé this. Cependant vous ne récupérez pas les prototypes pour autant. Pour cela il le spécifier avec la ligne suivante, tout de suite après le constructeur.

var ClassFille = function (p1, p2) {
    ClassMere.call(this, p1, p2);
}

ClassFille.prototype = new ClassMere();

La classe fille va avec cette ligne, hériter des prototypes de la classe mère, l’héritage est terminé. Par contre il faut prendre une chose en considération, une chose très importante.. une chose qui pourrait bien être à l’origine de tout un tas de bugs… Prenons un exemple.

var ClassMere = function (x, y) {
    var d = document.createElement("div");
    d.style.position = "absolute";
    d.style.top = y + "px";
    d.style.left = x + "px";
    document.body.appendChild(d);
};

var ClassFille = function (p1, p2) {
    ClassMere.call(this, p1, p2);
}
ClassFille.prototype = new ClassMere();

Vous savez ce qui cloche dans ce code ? Lorsque l’on va créer une instance de ClassFille alors on va créer avant une instance de la classe mère, qui elle même, va comme le code le montre, créer un élément div et l’attacher au DOM. Le problème vient à l’héritage des prototypes, on écrit ClassFille.prototype = new ClassMere()… Donc une deuxième fois un constructeur de ClassMere est appelé et va encore une fois créer et ajouter un élément au document ! Pour éviter ce genre de problème, et c’est valable dans d’autres langages : Utilisez une méthode d’initialisation ou alors si le constructeur contient des paramètres, vérifiez si il ne sont pas undefined !

Correction méthode n°1

var ClassMere = function (x, y) {
    this._x = x;
    this._y = y;
};

// Vous devrez l'appeler manuellement
ClassMere.prototype.initialize = function () {
    var d = document.createElement("div");
    d.style.position = "absolute";
    d.style.top = this._y + "px";
    d.style.left = this._x + "px";
    document.body.appendChild(d);
};

var ClassFille = function (p1, p2) {
    ClassMere.call(this, p1, p2);
}
ClassFille.prototype = new ClassMere();

// Et là plus de double élément dans le DOM, par contre il faudra appeler cette méthode manuellement.

Correction méthode n°2

var ClassMere = function (x, y) {
    // Si les paramètres existes alors on construit l'élément.
    if (x && y) {
        var d = document.createElement("div");
        d.style.position = "absolute";
        d.style.top = y + "px";
        d.style.left = x + "px";
        document.body.appendChild(d);
    }
};

var ClassFille = function (p1, p2) {
    ClassMere.call(this, p1, p2);
}
ClassFille.prototype = new ClassMere();

Vous noterez dans la méthode numéro 2 que j’ai simplement testé l’existence des deux variables comme suit (x && y), sachez que ça ne sert à rien de tester le type en JavaScript dans ce cas là, c’est même mal car c’est parfois trompeur. Effectivement une variable peut valoir null, undefined, false ou 0 et un test strict ne passera pas à chaque fois.

Hériter autrement avec Object.create

La troisième solution pour éviter ce genre de problème est d’utiliser Object.create à la place de new ClassMere(), c’est donc la deuxième possibilité pour faire de l’héritage. Le fait de créer une deuxième instance est problématique et nous avons vue pourquoi plus haut. Avec Object.create nous allons faire un héritage des prototypes uniquement. La seule différence est là :

ClassFille.prototype = Object.create(ClassMere.prototype);

La classe Player devient donc

Player.js (modifiée)

var Player = function (name, x, y, job) {
	Sprite.call(this, name, x, y);
	this._job = job;
};

Player.prototype = Object.create(Sprite.prototype);

Player.prototype.draw = function (spriteBatch) {
	Sprite.prototype.draw.call(this, spriteBatch);
	console.log("Je dessine en plus une icone du job");
};

var player = new Player ("player", 15.0, 50.0, "Knight");
console.log(player.draw());

Et là c’est déjà beaucoup mieux car nous n’avons qu’une instance de la classe mère qui est créée ! C’est donc la méthode à utiliser. Sachez par contre que Object.create n’est pas utilisable en dessous d’Internet Explorer 9, il y a cependant des polyfill mais gardez ça en tête.

Enfin, car nous avons bientôt terminé, la surcharge de la méthode draw() est intéressante car on a besoin d’appeler la méthode de la classe mère. En Java on utilise le mot clé super suivi de la méthode à appeler. En JavaScript on utilise le nom de classe (avec les namespaces avant si il y en a) suivi du mot clé prototype, puis le nom de la fonction et enfin call. En premier paramètre il faudra spécifier l’instance de l’objet en cours, donc this, ça, ça ne change pas 😉 et enfin les paramètres si il y en a.

Récapitulatif sur le mot clé super et son équivalent en JavaScript

// En Java
super(x, y); // Dans le constructeur
super.initialize(); // Dans la méthode initialize
super.draw(spriteBatch); // Dans la méthode draw

Sprite.call(this, x, y); // Dans le constructeur
Sprite.prototype.initialize.call(this); // Dans la méthode initialize
Sprite.prototype.draw.call(this, spriteBatch); // Dans la méthode draw

Bon et bien mine de rien on vu déjà pas mal de choses, on va terminer avec les variables et méthodes statiques et quelques mots clé à connaitre.

4. Méthodes et membres statiques

Cette partie est définitivement la plus simple ! Il peut arriver que vous ayez besoin d’avoir recourt à une variable statique pour par exemple compter le nombre d’instances d’un objet. On peut faire cela comme ça :

var MonObjet = function() {
    // Initialisation de l'objet
    this.id = MonObjet.Counter++;
};

MonObjet.Counter = 0;

console.log(new MonObjet().id); // 0
console.log(new MonObjet().id); // 1
console.log(new MonObjet().id); // 2

Déclarer une méthode statique revient à déclarer une méthode prototype, sans le mot clé prototype :O

var MonObjet = function() {};

MonObjet.logSomething = function () {
  console.log("I'm a static function man !");
}

MonObjet.logSomething();

Enfin pour finir, je dois vous parler des mots clé instanceof et typeof car ils sont utiles mais trompeur parfois..

Le mot clé instanceof

Il permet de vérifier si un objet est d’un type donné, par exemple vous pouvez écrire :

var player = new Player("player", 0, 0, "Knight");
var sprite2 = new Sprite("item", 10, 10);
var div = document.getElementsByTagName("div")[0];

console.log(player instanceof Sprite); // true
console.log(player instanceof Player); // true
console.log(sprite instanceof Sprite); // true
console.log(sprite instanceof Player); // false
console.log(div instanceof HTMLElement); // true
console.log(div instanceof HTMLCanvasElement); // false

La variable player est de type Sprite et par extension de type Player, par contre la variable sprite n’est que de type Sprite. Ce mot clé fonctionne avec tous les types définis, que ce soit les vôtres ou ceux disponibles de base. C’est pratique pour tester un objet du DOM par exemple.

Le mot clé typeof

Alors celui-là est traître au possible et il faudra vous en méfier (vous m’entendez ?). Il n’existe pas de méthode permettant de récupérer le nom d’une classe, typeof va simplement vous dire de quel type est la variable que vous lui passez en paramètre.

typeof(1); // number
typeof("1"); // string
typeof(+"1"); // number
typeof("Hello"); // string
typeof(sprite); // object

Conclusion

Comme vous avez pu le constater à travers ce tutoriel, le langage JavaScript est différent du langage Java, en réalité il est différent, point barre. Il ne faut pas partir avec des préjugés et vouloir le comparer automatiquement à d’autres langages, mais le prendre comme il est, et apprendre à en tirer bénéfice car croyez moi, un paquet de choses bien peuvent être faites avec du JavaScript ! Comme dans tous les langages d’ailleurs.

Sachez qu’il existe un tas d’autres choses que vous pouvez faire pour structurer votre code et surtout le sécuriser. Je pense aux closures dans un premier temps mais il y a aussi les namespaces et puis tout un tas d’autres techniques à apprendre pour se perfectionner. La documentation de Mozilla est je pense la meilleure que l’on puisse trouver sur le web et je vous encourage vivement à y aller, il y a presque tout ce que vous voulez dessus. La MSDN est aussi très pratique et complète.

N’hésitez pas à venir débattre et poser vos questions dans les commentaires ou alors sur le meilleur endroit des internets pour discuter entre passionnés : Google+ !

Un grand merci à Alex pour avoir prit le temps de relire cet article.