Source : https://gist.github.com/ychaouche/3218763

Javascript orienté objet

L'orientation objet du javascript


Il n'y a pas de notion de classe en javascript. Il n'y a que des objets. L'héritage se fait entre objets et non entre classes, c'est ce qu'on appelle l'héritage par prototype (où le prototype d'un objet est l'objet duquel il hérite. Analogiquement en orientation objet classique, on parle de classe mère ou classe parente, ici on parle de prototype).

Ceci a pour conséquence que l'abstraction du programme en "classes" desquelles on instancie des objets n'existe pas en tant que tel en javascript. Au lieu de ça, on a le choix entre soit créer un objet via le mot clé "new" suivi du constructeur de l'objet (analogiquement aux constructeurs de classes dans la OO classique), ou bien en utilisant la nouvelle fonction Javascript (introduite en ECMAScript 5) dédiée à la création d'objets qui est "Object.create".

Création d'un objet en Javascript


Nous avons donc dit qu'il y avait deux façons de faire.

1) Création d'objets via Object.create()

Comme ceci :

<script>
var damien     	        = Object.create(null);	// créé un objet vide.
damien.nom	        = "Damien";
damien.prenom	        = "De la Rochefoucauld";
damien.get_denomination = function () {return this.nom + " " + this.prenom};


console.debug(damien);// affiche "[nom,prenom,get_denomination,] { nom="Damien", prenom="De la Rochefoucauld", get_denomination=function()}"
console.debug(damien.nom); // affiche "Damien"
console.debug(damien.get_denomination()); // affiche "Damien De la Rochefoucauld"
</script>

Comme vous avez pu le voir avec la ligne…

<script>
damien.get_denomination = function () {return this.nom + " " + this.prenom};
</script>

…les fonctions sont des citoyens de première classe en Javascript et peuvent donc être mises dans des variables, passées en paramètres, retournées par d'autres fonctions, être construites pendant l'execution etc.

la mot clé this

Nous avons utilisé le mot clé this dans la méthode damien.get_denomination. En Javascript, this se comporte différement suivant comment la fonction qui le contient est invoquée. Reprenons notre exemple de tout à l'heure :

<script>
var damien     	        = Object.create(null);	// créé un objet vide.
damien.nom	        = "Damien";
damien.prenom	        = "De la Rochefoucauld";
damien.get_denomination = function () {return this.nom + " " + this.prenom};
</script>

Si on appelle la fonction de cette manière

<script>
damien.get_denomination();
</script>

alors this sera liée à l'objet damien.

prenons un autre example :

<script>
var damien     	        = Object.create(null);	// créé un objet vide.
damien.nom	        = "Damien";
damien.prenom	        = "De la Rochefoucauld";
damien.get_denomination = function () {return this.nom + " " + this.prenom};

function auto_flatterie(personne){
   return personne.get_denomination() + " est le meilleur !";
}

console.debug(auto_flatterie(damien)); // affiche "Damien De la Rochefoucauld est le meilleur !"
</script>

Jusque là tout est logique. La fonciton auto_flatterie prends une personne comme paramètre, obtient sa dénomination et lui ajoute " est le meilleur" qu'elle retourne comme résultat.

Mainteant, écrivons auto_flatterie autrement :

<script>
var damien     	        = Object.create(null);	// créé un objet vide.
damien.nom	        = "Damien";
damien.prenom	        = "De la Rochefoucauld";
damien.get_denomination = function () {return this.nom + " " + this.prenom};

function auto_flatterie(){
   return this.get_denomination() + " est le meilleur !";
}

console.debug(damien.auto_flatterie()); // affiche "TypeError: damien.auto_flatterie is not a function"
</script>

Effectivement, l'auteur de ce code a essayé d'appeler la fonction auto_flatterie à damien comme si c'était une méthode de cet objet. Or, comme auto_flatterie a été déclarée seule, et non comme méthode de damien, elle reste introuvable dans l'objet damien. Au lieu de ça, l'auteur aurait du utiliser la méthode "call" présente dans toutes les fonctions (en javascript, les fonctions sont des objets et peuvent donc avoir des méthodes comme c'est le cas ici).

<script>
var damien     	        = Object.create(null);	// créé un objet vide.
damien.nom	        = "Damien";
damien.prenom	        = "De la Rochefoucauld";
damien.get_denomination = function () {return this.nom + " " + this.prenom};

function auto_flatterie(){
   return this.get_denomination() + " est le meilleur !";
}

console.debug(auto_flatterie.call(damien)); // affiche bien "Damien De la Rochefoucauld est le meilleur !"
</script>

call prends comme premier argument l'objet sur lequel la fonction va être appelée. Le reste des arguments passés à call vont être passés tels quels en argument à la fonction appelée. Ici, l'objet "damien" va être "bindé" à la valeur de "this" dans la fonction "auto_flatterie". Si auto_flatterie prenait un argument, il sera "bindé" au second argument passé à la fonction "call", par exemple :

<script>
var damien     	        = Object.create(null);	// créé un objet vide.
damien.nom	        = "Damien";
damien.prenom	        = "De La Rochefoucauld";
damien.get_denomination = function () {return this.nom + " " + this.prenom};

function auto_flatterie(flatterie){ // auto_flatterie prends maintenant un argument
   return this.get_denomination() + " est " + flatterie + " !";
}

console.debug(auto_flatterie.call(damien,"le plus beau")); // affiche  "Damien De la Rochefoucauld est le plus beau !"
</script>

Maintenant si la fonction est appelée sans rien en paramètre, le mot clé this prends l'objet par défaut qui est la fenêtre du navigateur ou l'objet "window".

<script>
function auto_flatterie(flatterie){ // auto_flatterie prends maintenant un argument
   return this.get_denomination() + " est " + flatterie + " !";
}

console.debug(auto_flatterie("le plus beau")); // affiche  "TypeError: this.get_denomination is not a function"
</script>

effecivement, this étant l'objet window, javascript ne trouve pas de méthode get_denomination associée.

<script>

function get_denomination(){
    return "Fabrice Petard";
}

function auto_flatterie(flatterie){ // 
   return this.get_denomination() + " est " + flatterie + " !";
}

console.debug(auto_flatterie("le plus intelligent")); // affiche  "Fabrice Petard est le plus intelligent !"
</script>

Cette fois-ci get_denomination est bien trouvée. En effet, comme get_denomination est déclarée dans une portée globale, elle est ajoutée comme propriété de l'objet global. Celui-ci est "window" dans les navigateurs ou "gloabal" sur les serveurs (node.js par exemple). Comme "this" à l'interieur de la fonction auto_flatterie est "bindé" à l'objet global, du fait que l'on ait appelé la fonction directement, this.get_denomination est donc équivalent à window.get_denomination, et le code marche parfaitement.

2) création d'objets via le mot clé "new" + un constructeur

Essayons de construire damien différement

<script>

function Personne (nom,prenom,age){ // notez que j'utilise ici une capitale...
    this.nom = nom;
    this.prenom = prenom;
    this.age = age;
}

damien = Personne("De la Rochefoucauld", "Damien", 29);

console.debug(damien); // affiche "undefined"

</script>

Hé oui, ça ne marche pas. Que fait ce code ? hé bien, après avoir défini la fonction Personne, nous faisons simplement un appel à cette fonction. Mais cette fonction ne retourne rien, donc damien reçoit undefined. Mais n'est-ce pas sensé retourner un objet dites-vous ? Non, rappelez-vous que Personne est juste une fonction, elle ne retourne que ce que vous mettez dans return. Pour construir l'objet damien, vous devez utiliser le mot clé "new" devant la fonction Personne.

<script>

function Personne (nom,prenom,age){ // notez que j'utilise ici une capitale...
    this.nom = nom;
    this.prenom = prenom;
    this.age = age;
}

damien = new Personne("De la Rochefoucauld", "Damien", 29);

console.debug(damien); // affiche Personne {nom="De la Rochefoucauld", prenom="Damien", age=29}

</script>

Ceci a pour effet de construir un objet pour vous et d'appeler immédiatement dessus la fonction Personne. A l'interieur de "Personne", les "this" représente l'objet fraîchement créé. Enfin, l'objet est retourné. Ainsi, les lignes

this.nom = nom;
this.prenom = prenom;
this.age = age;

vont créér les propriétés nom, prenom et age dans l'objet nouvellement créé par new. (la notion de "propriété" dans javascript regroupe à la fois les notions d'attributs et de méthodes dans la OO classique).

ceci est donc équivalent à écrire par exemple :

<script>
henry = Object.create(null);
Personne.call(henry,"Razoir","Henry",29);
console.debug(henry); // affiche [nom,prenom,age,] { nom="Razoir", prenom="Henry", age=29}
</script>

Attendez une minute… Pourquoi est-ce que je n'obtiens pas le même résultat que pour damien ?

console.debug(damien); // affiche Personne {nom="De la Rochefoucauld", prenom="Damien", age=29}
console.debug(henry);  // affiche [nom,prenom,age,] { nom="Razoir", prenom="Henry", age=29}

Pour damien j'obtiens Personne {nom = …} et pour Henry seulement [nom,prenom,age] = {nom=…} pourquoi ?

Eh bien parce que comme nous avons mis le mot clé "new" devant la fonction Personne, elle sera considérée comme le constructeur de l'objet damien. En revanche, l'objet henry n'a pas de constructeur puisqu'il a été créé à l'aide de Object.create() et non d'un "new".

<script>

function Personne (nom,prenom,age){
    this.nom = nom;
    this.prenom = prenom;
    this.age = age;
}

damien = new Personne("De la Rochefoucauld", "Damien", 29);
</script>

Ajout de méthodes

A présent, ajoutons la fonction get_denomination comme propriété de notre objet damien (et deviendra donc une méthode). Nous pouvons faire simplement comme tout à l'heure.

<script>

function Personne (nom,prenom,age){
    this.nom = nom;
    this.prenom = prenom;
    this.age = age;
}

damien = new Personne("De la Rochefoucauld", "Damien", 29);
damien.get_denomination = function () {return this.nom + " " + this.prenom};
console.debug(damien.get_denomination()); // affiche "De la Rochefoucauld Damien".
</script>

Pourquoi ne pas faire profiter henry de cette même méthode ?

<script>

function Personne (nom,prenom,age){
    this.nom = nom;
    this.prenom = prenom;
    this.age = age;
}

damien = new Personne("De la Rochefoucauld", "Damien", 29);
damien.get_denomination = function () {return this.nom + " " + this.prenom};

henry = Object.create(damien);
Personne.call(henry,"Razoir","Henry",20);
console.debug(henry.get_denomination()); // affiche bien Razoir Henry
</script>

Comment ça se fait ? je ne vous ai pas dit que Object.create prenait un paramètre. Il en prends même plusieurs. Mais pour ce qui nous intéresse, seul le premier compte. C'est ce premier paramètre qui va être bindé comme *prototype* (ou parent) de l'objet retourné. La ligne

henry = Object.create(damien);

Va créér un objet qui "hérite" de l'objet damien et qui aura donc accés à toute ses propriétés : nom, prenom, age ainsi que get_denomination. C'est pourquoi je fais dans un second temps :

Personne.call(henry,"Razoir","Henry",20);

Pour que henry soit distinguable de damien.

Je vous vois déjà soufrir. "Quelle est cette idée saugrenue de définir une méthode comme propriété d'un objet particulier ? pourquoi ne pas la rendre générale à tous les objets de type Personne ?"

Attention, vous vous accrochez encore aux concepts OO classiques. Personne n'est pas un type, ni une classe. "Mais comment partager une méthode entre plusieurs objets alors ?".

Une première idée qui pourrait venir à l'esprit serait d'écrire quelque chose comme :

<script>
function Personne (nom,prenom,age){
    this.nom = nom;
    this.prenom = prenom;
    this.age = age;
    this.get_denomination = function(){return this.nom + " " + this.prenom};
}

damien = new Personne("De la Rochefoucauld", "Damien", 29);
henry  = new Personne("Razoir","Henry",20);
console.debug(damien.get_denomination()); // affiche De la Rochefoucauld Damien
console.debug(henry.get_denomination());  // affiche Razoir Henry
</script>

Mais à chaque fois qu'on créé un nouvel objet de type Personne, une nouvelle fonction (anonyme) est créé et est mise dans sa propriété get_denomination. On obtient donc *plusieurs copies de cette fonction*, une pour chaque objet. "Mais ce n'est pas ce que je veux" pleurez-vous, "la fonction devrait être définie une seule fois pour tout le monde, et non une fois par objet".

Exactement et c'est le but. J'essaye simplement de vous montrer qu'il existe plusieurs manières de se tromper. C'est en documentant toutes les erreurs que j'ai faites que j'ai réussi à comprendre le Javascript orientée objet. C'est important, pour se débarasser des concepts de la OO classiques. "Mais alors, pourquoi ne pas définir get_denomination comme propriété de Personne ?", ok, essayons :

<script>
function Personne (nom,prenom,age){
    this.nom = nom;
    this.prenom = prenom;
    this.age = age;
    Personne.get_denomination = function(){return this.nom + " " + this.prenom};
}

damien = new Personne("De la Rochefoucauld", "Damien", 29);
henry  = new Personne("Razoir","Henry",20);
...
</script>

"En définissant get_denomination comme une propriété de Personne, tous les objets en héritant vont y avoir accès, n'est-ce pas ?", dites-vous avec des yeux espérant mon acquiescement et un sourir hésitant.

<script>
...
console.debug(damien.get_denomination()); // affiche TypeError: damien.get_denomination is not a function
console.debug(henry.get_denomination());  // affiche TypeError: henry.get_denomination is not a function
</script>

Râté ! Encore une des sequelles de la OO classique. Personne n'est ni une classe, ni un type, rappelez-vous bien. C'est une fonction qui joue le rôle d'un constructeur dans les notions classiques de l'orienté objet. Un constructeur, rappelez vous, n'est executé qu'après qu'un objet ait été créé.

Ici, nous avons défini get_denomination comme propriété de la fonction Personne. La ligne

damien = new Personne("De la Rochefoucauld", "Damien", 29);

ne créé pas un objet *de type personne* en passant ("De la Rochefoucauld", "Damien", 29) comme paramètre. Non. Elle créé un objet *(tout court)* et lui applique le constructeur Personne qui lui ajoute les propriétés nom prénom et age. L'objet créé n'a aucun moyen d'accéder à ce qui est défini directement dans Personne. Par contre, il a accès à tout ce qui est défini dans Personne.prototype…

Prototype

Pour définir une fonction qui soit commune à tous les objets retournés par un constructeur (Personne dans notre cas), on peut la définir dans le prototype de ce constructeur. C'est ici que les objets créés vont chercher les propriétés qui ne se trouvent pas en eux même (ces dernières sont appelés *own* properties).

<script>
function Personne (nom,prenom,age){
    this.nom = nom;
    this.prenom = prenom;
    this.age = age;
    Personne.prototype.get_denomination = function(){return this.nom + " " + this.prenom};
}

damien = new Personne("De la Rochefoucauld", "Damien", 29);
henry  = new Personne("Razoir","Henry",20);
console.debug(damien.get_denomination()); // affiche De la Rochefoucauld Damien
console.debug(henry.get_denomination());  // affiche Razoir Henry
</script>

En déclarant get_denomination comme étant une propriété non pas de Personne, mais de Personne.prototype, la fonction (anonyme) est stockée une seule fois à un seul endroit (dans Personne.prototype) auquel tous les objets créés via new Personne(…) auront accès. Ceci évite la duplication en mémoire de cette propriété dans chaque objet comme nous l'avons fait au début avec la ligne :

  this.get_denomination = function(){return this.nom + " " + this.prenom};

dans la fonction Personne.

Essayez de voir un prototype comme étant un espace ou tous les objets créés par le constructeur associé ont accès pour la recherche de propriétés qui ne sont pas définis au niveau d'eux même.

Une question de style

Il y a un problème avec le code précédent :

<script>
function Personne (nom,prenom,age){
    this.nom = nom;
    this.prenom = prenom;
    this.age = age;
    Personne.prototype.get_denomination = function(){return this.nom + " " + this.prenom};
}
</script>

Nous avons déclaré la fonction get_denomination à l'intérieur de Personne, qui est un constructeur. Vous ne faites pas ça dans les autres langages, n'est-ce pas ? effectivement, un contructeur en général est utilisé pour "instancier" un nouvel objet, basta. Si je compare ça à d'autres langages comme le C++, la déclaration de la classe se fait à part, et l'implémentation à part.

Par exemple en C++,

// la déclaration

class Personne {
   public:
   std::string nom;
   std::string prenom;
   Personne(std::string,std::string);
   void bonjour();
};
 
// puis l'implémentation

Personne::Personne(std::string nom, std::string prenom){
   this->nom    = nom;
   this->prenom = prenom;
}
 
void Personne::bonjour (){
   std::cout << "bonjour, je suis " << nom + " " + prenom;
}

En Javascript

<script>
function Personne (nom,prenom,age){
    this.nom = nom;
    this.prenom = prenom;
    this.age = age;
}
Personne.prototype.get_denomination = function(){return this.nom + " " + this.prenom};

// ou encore

function nom_prenom(){
    return this.nom + " " + this.prenom
}

Personne.prototype.get_denomination = nom_prenom
console.debug(damien.get_denomination()); // affiche bien : De la Rochefoucauld Damien.
</script>

Les constructeurs

Chaque fonction javascript se voit attribuer une propriété "prototype". Dans cette propriété, javascript créé automatiquement une propriété constructeur qui a pour valeur la fonction elle même.

<script>
function F(){};
console.debug(F.prototype);                  // => F {} , c'est un objet; qui sera le [[prototype]] de l'objet créé par new F();
console.debug(F.prototype.constructor);      // => F()  , c'est une fonction, la fonction F.
console.debug(F.prototype.constructor == F); // => true 
</script>

Différence entre prototype et [[prototoype]]

- "prototype" désigne une propriété, la propriété d'une fonction qui contient un objet
- [[prototype]] désigne un objet, comme dit tout au début de ce cours, quand vous faites

<script>
function Personne (nom,prenom,age){ 
    this.nom = nom;
    this.prenom = prenom;
    this.age = age;
}

damien = Personne("De la Rochefoucauld", "Damien", 29);
henry  = Object.create(damien);
</script>

Ici, le [[prototype]] de henry est damien.

- damien : "Je suis ton père"
- henry : "Nooooooooooooon !"

C'est tout bon ! ou bien … ?

Vérifiez que vous avez tout compris en répondant au quizz ci-dessous

QUIZZ Répondre par vrai ou faux :

  1. prototoype est une propriété des fonctions
  2. [[prototype]] est une prorpiété des objets
  3. [[prototype]] est inacessible
  4. Le concepte de méthode n'existe pas en javascript
  5. Les méthodes sont généralement déclarées dans la propriété prototype d'un constructeur.
  6. On ne peut pas déclarer une méthode en javascript. C'est simplement au moment de l'appel d'une fonction qu'on determine si elle sera appelée comme une méthode ou pas. Correct ?
  7. Chaque objet javascript possède une propriété "constructor";

Réponses

  1. vrai
  2. vrai
  3. vrai
  4. Faux. Javascript possède bien un concept de méthodes. Selon les specs, une méthode en javascript est simplement une fonction qui est une valeur d'une propriété.
  5. oui, mais elles peuvent être ajoutées à tout moment à un objet. Une fonction deviens méthode simplement au moment de l'appel.
  6. Faux, on peut créer une méthode avec Function.bind, par exemple sur un constructeur.
  7. Faux, ce n'est pas une propriété dans l'objet lui même mais dans son prototype. Exemple :

<ychaouche> » o = Object.create(null); o.constructor;
<ecmabot> ychaouche: undefined

ecmabot est un bot IRC qui execute du code javascript. Vous pouvez lui parler sur #javascript simplementt en précédent votre message par un double chevron comme montré plus haut dans l'exemple.


QR Code
QR Code oojs (generated for current page)