es5-prototype.png

ES5 - Les prototypes

par Romain · · pas de commentaire

Rappel ES5 - Les prototypes

Dans une volonté d'en apprendre plus sur javascript, bien souvent nous partons sur l'apprentissage d'un nouveau framework, ces frameworks souvent accompagnés de cli permettent directement de pouvoir travailler en EcmaScript 2015+ (à travers babel) afin d'obtenir des applications "modernes".

Cependant, je remarque que la plupart du temps il est facile d'appliquer des principes et d'effectuer des tâches en javascript sans pour autant comprendre ce qu'il se passe sous le capot, d'autre part dans un cadre pédagogique où l'on souhaiterait enseigner ces nouveaux concepts à des personnes qui ont pratiqués du javascript à l'ancienne, il est bien difficile de pouvoir créer des analogies si les concepts ES5 sont inconnus.

Cet article est le premier d'une longue série afin de faire une piqure de rappel sur l'ancienne syntaxe de javascript que l'on appelle ES5 et qui a mon sens il est important de maitriser.

Les prototypes

Introduction

Tout d'abord pour bien se rendre compte de l'utilité d'un prototype. Nous allons commencer par créer un array et récupérer le dernier element de cet array.

"use strict";
var arr = ["rouge", "bleu", "vert"];
var last = arr[arr.length - 1];
console.log(last); // vert

Maintenant je vais vous proposer de rajouter une fonction a notre array qui permettra de récupérer le dernier élement de la façon suivante :

...
var last = arr.last
console.log(last)

Bien evidemment Array ne possède pas la propriété last en javascript, mais comme javascript est dynamique on pourrait ajouter une propriété qui me calculerait le dernier element (un accesseur).

"use strict";
var arr = ["rouge", "bleu", "vert"];
Object.defineProperty(arr, "last", {
  get: function () {
    return this[this.length - 1];
  },
});

var last = arr.last;
console.log(last);

Cela marche très bien, cependant si je créer un nouvel array je ne dispose plus de ma propriété 'last' Il est possible de définir directement cette propriété sur le prototype de l'objet array

On peut faire cela en changeant l'instance de l'objet par le prototype de Array :

"use strict";
var arr = ["rouge", "bleu", "vert"];
Object.defineProperty(Array.prototype, "last", {
  get: function () {
    return this[this.length - 1];
  },
});

var last = arr.last;
console.log(arr.last);
var arr2 = ["un", "deux", "trois"];
console.log(arr2.last);

Ça y est ! Maintenant tous les arrays disposent de ma propriété last

Qu'est-ce que cet objet Array ?

Si on regarde on voit que c'est juste une fonction, une fonction qui semble être utilisée en tant que constructeur. (voir Les objets) La déclaration de l'array ['rouge', 'bleu', 'vert']; est un raccourci de l'appel de la fonction constructeur Array

// équivalent var arr = ["rouge", "bleu", "vert"];
var arr = new Array("red", "blue", "green");

Qu'est-ce que Array.prototype et qu'est-ce qu'un prototype ?

Un prototype est un objet qui existe sur toute les fonctions en javascript. Notez que même si je crée la plus basique de toutes les fonctions, elle possède déjà un prototytpe.

var mafunc = function () {};
console.log(mafunc.prototype);

var chien = { nom: "beckie" };
console.log(chien.__proto__);

Remarque : les objets eux, n'ont pas de propriété prototype cependant ils ont une propriété __proto___

Donc en réalité, les objets ont un prototype qui leur est assigné. Mais Un prototype d'objet et un prototype de fonction sont utilisés différemment.

Définition

Un prototype de function : Un prototype de fonction est l'instance d'objet qui va devenir le prototype de tous les objets crées en utilisant cette fonction en tant que constructeur.

Un prototype d'objet : Un prototype d'objet est l'instance d'objet à partir duquel l'objet a été crée.

Quand un objet est crée avec le mot clé new à travers une fonction Constructeur, alors on lui attache un objet proto qui pointe vers l'objet prototype de la fonction contructeur, c'est plus simple à comprendre avec le code, donc démarrons le code.

Manipuler le prototype

"use strict";
function User(name, age) {
  this.name = name;
  this.age = age;
}

console.log(User.prototype);

Maintenant nous allons créer un objet User et afficher le prototype de ma fonction constructeur et le prototype de l'objet construit.

"use strict";
function User(name, age) {
  this.name = name;
  this.age = age;
}

var romain = new User("Romain", 31);
console.log(User.prototype);
console.log(romain.__proto__);

Ils ont tous les deux la même forme !

Mais ils n'ont pas seulement la même forme, ils poitent vers la même instance d'objet.

// renvoie true uniquement si même instance
User.prototype === romain.__proto__; // true

Maintenant, changeons pour voir le prototype de la fonction User et affichons encore le prototype de la fonction constructeur ainsi que celui des instances d'user

"use strict";
function User(name, age) {
  this.name = name;
  this.age = age;
}

var romain = new User("Romain", 31);
User.prototype.address = "rue du test";
console.log(User.prototype);
console.log(romain.__proto__);

var paul = new User("Paul", 30);
console.log(paul.__proto__);

On voit que si je change le prototype de la fonction constructeur User, c'est automatiquement pris en compte pour le prototype de l'objet. Et c'est normal puisque je vous rappelle que le prototype de l'objet est un pointeur vers le prototype de la fonction constructeur User. En créant un nouvel user, je m'assure que c'est toujours vrai si je crée une nouvelle instance de User.

Maintenant nous allons voir ce qui est réellement arrivé à la propriété address de notre prototype.

Les propriétés d'instance et les propriétés de prototype

Quand on ajoute une propriété à un prototype il est disponible par tous les objets qui partagent ce même prototype, mais ou est-ce que cette property vit ? On part de l'exemple suivant avec deux utilisateurs, en théorie si j'ajoute un role au prototype de la fonction constructeur User on devrait le role de tous les utilisateurs qui pointent vers ce prototype.

Mais que se passe-t'il si j'ajoute un role pour l'un des deux utilisateurs ?

"use strict";
function User(name, age) {
  this.name = name;
  this.age = age;
}

var romain = new User("Romain", 31);
var paul = new User("Paul", 30);

paul.role = "gestionnaire";

console.log("structure de l'objet paul ?", paul);
console.log("récupération de paul.role", paul.role);
console.log("récupération de paul.__proto__.role", paul.__proto__.role);
console.log("structure de l'objet romain ?", romain);
console.log("récupération de romain.role", romain.role);
console.log("récupération de romain.__proto__.role", romain.__proto__.role);

Résultat :

  • On voit que l'utilisateur paul possède deux rôles celui de l'objet et celui de son prototype.
  • On voit que l'utilisateur romain possède un rôle celui de son prototype.

Dans le cas de la variable romain, quand on demande la propriété role de l'objet qui n'existe pas, javascript nous renvoit le role de son prototype! La priorité est donnée à la propriété de l'objet puis si elle n'existe pas javascript va regarder dans le prototype

Si je n'avais pas mis de role dans l'objet paul alors il ne possèderait pas de propriété role et nous allons le démontrer :

"use strict";
function User(name, age) {
  this.name = name;
  this.age = age;
}

User.prototype.role = "administrateur";

var romain = new User("Romain", 31);
var paul = new User("Paul", 30);

console.log(paul.role); // administrateur
console.log(paul.__proto__.role); // administrateur
console.log(Object.keys(paul)); // ["name", "age"] pas de role...
console.log(paul.hasOwnProperty("role")); // false

Javascript ne trouvant pas de propriété role pour l'objet paul ira alors chercher celle du prototype lorsque l'on accède à la propriété

Changer le prototype d'une fonction

Hé Romain, dis moi si je change le prototype d'une fonction constructeur, cela change il la réference du prototype des objets crées par cette même fonction contructeur ?

Pour rappel le prototype est l'instance d'objet associé à une fonction qui va devenir le prototype pour tous les objets crées en utilisant cette même fonction. Pour répondre à cette question il va être nécessaire de faire un test

"use strict";
function User(name, age) {
  this.name = name;
  this.age = age;
}

User.prototype.address = "3 rue du temple";
var romain = new User("Romain", 31);
var paul = new User("Paul", 30);

User.prototype = { address: "6 rue de la soif" };

var pierpoljak = new User("Pierre", 50);
console.log(romain.address);
console.log(paul.address);
console.log(pierpoljak.address);
console.log(User.prototype.address);

Résultat :

Bizarre non ? Que se passe-t'-il ? Voici un schéma illustrant ce qu'il se passe.

La fonction User ne pointe plus vers son propre prototype, cependant le prototype original existe toujours et est pointé par les instances d'objet !

Créer son propre héritage prototypal

Maintenant je souhaiterais pouvoir reproduire le principe d'héritage avec les prototypes.

Qu'est-ce que l'héritage ?

Lorsque nous allons créer nos objets à travers une fonction constructeur ou bien une classe ES6, il est parfois utile de pouvoir créer un prototype à partir d'un prototype déjà existant.

Par exemple nous allons vouloir créer un constructeur Animal, et nous allons rajouter a son prototype une fonction commune à tous les animaux. Lorsque je vais créer ma fonction constructeur Chien je souhaiterais (vu que le Chien est un animal) avoir par défaut cette fonction.

Comment faire alors si je veux que le prototype de ma classe "Chien" hérite du prototype de ma fonction constructeur "Animal" ? On va utiliser Object.create afin d'effectuer un héritage de classe.

"use strict";

//création de notre fonction constructeur Animal
function Animal() {}

// chaque fonction possède un prototype, nous allons ajouter une fonction "manger" à ce prototype
Animal.prototype.manger = function () {
  console.log("chomp chomp");
};

function Chien(nom, couleur) {
  this.nom = nom;
  this.couleur = couleur;
}

// grace à Object.create, je peux créer un prototype à partir d'un autre
Chien.prototype = Object.create(Animal.prototype);
var oswald = new Chien("oswald", "orange");

oswald.manger(); // affiche "chomp chomp"

// Est-ce que oswald est un Animal ?
console.log(oswald instanceof Animal); // true

//Est-ce que oswald est un Chien ?
console.log(oswald instanceof Chien); // true

La fonction "manger", n'est pas une propriété de "Chien", donc la fonction "hasOwnProperty" retournerait false par exemple. Mais c'est une propriété de son prototype "Chien" qui a hérité du prototype "Animal"

Ok c'est très bien, mais dans le cas où la fonction constructeur "Animal" possède une valeur d'initialisation, comment l'initialiser dans "Chien" ?

Avec la surchage de constructeur :

"use strict";

function Animal(bruit) {
  this.bruit = bruit || "Crunch";
}
Animal.prototype.manger = function () {
  console.log(this.bruit);
};

function Chien(nom, couleur, bruit) {
  // equivalent d'un super en java par exemple
  Animal.call(this, bruit);
  this.nom = nom;
  this.couleur = couleur;
}

// creation d'un prototype pour la fonction constructeur "Chien"
Chien.prototype = Object.create(Animal.prototype);
// Par default le constructeur du prototype Chien est une copie de celui de Animal.
// Il faut lui réattributer le bon constructeur.
Chien.prototype.constructor = Chien;
var oswald = new Chien("oswald", "orange", "chomp chomp");
var junior = new Chien("junior", "black");
oswald.manger(); //chomp chomp
junior.manger(); // Crunch

Est-il possible d'hériter de plusieurs parents ?

Non , javascript ne supporte que l'héritage unique, cependant on peut utiliser les mixins.

Equivalent avec les classes

Tout ce qui est déclaré avec les classes est une façon douce de voir ce que l'on vient de faire avec la chaine des prototypes. à une ou deux différences près.

Création de la classe Animal

Un constructeur qui va prendre un bruit par defaut, et une méthode manger() Ensuite on va définir notre classe Chien qui va étendre de notre classe Animal

"use strict";
class Animal {
  constructor(bruit) {
    this.bruit = bruit || "Crunch";
  }

  manger() {
    console.log(this.bruit);
  }
}

class Chien extends Animal {
  constructor(nom, couleur, bruit) {
    super(bruit);
    this.nom = nom;
    this.couleur = couleur;
  }
}
var oswald = new Chien("oswald", "orange", "chomp chomp");
var junior = new Chien("junior", "black");
oswald.manger();
junior.manger();

Voilà pour le petit topo sur les prototypes en javascript, si vous souhaitez aller plus loin voici une liste d'articles connexes: