Les prototypes ES5 et les classes ES6
10 January 2020
Protoype d'une fonction et proto d'un objet
Débutons tout d’abord par une définition très simpliste de Prototype = objet qui existe sur chaque fonction JavaScript.
Notre constructeur Vehicle n’échappe pas à la règle et possède lui aussi un prototype.
// prototype de notre fonction constructeur Vehicle
console.log(Vehicle.prototype);
// { constructor: f }
// constructor: f Vehicle(brand, model, color)
// __proto__: Object
Admettons que nous souhaitons que toutes nos instances de Vehicle soient par
défaut des véhicules français. Nous allons pouvoir ajouter cette propriété au prototype de notre constructeur Vehicle.
// ajout d'une propriété à notre constructeur Vehicle
Vehicle.prototype.country = 'France';
console.log(Vehicle.prototype);
// { country: ‘France’, constructor: f }
Désormais, chaque instance du constructeur Vehicle aura dans son proto le paramètre “country” avec la valeur “France”.
On accède ainsi au prototype d’un objet:
// __proto__ d'un objet
console.log(vehicle.__proto__)
// { country: ‘France’, constructor: f }
Nous nous apercevons donc bien ici, que la propriété “country” est présente dans le prototype du constructeur, ainsi que dans le prototype de chaque future instance.
// comparaison d'une prototype et de __proto__
Vehicle.prototype === vehicle.__proto__
// TRUE
L’intérêt de la chose est que, si on souhaite que toutes les instances créées à l’aide du constructeur disposent de certaines propriétés ou certaines fonctions, il faut les ajouter au prototype du constructeur.
// update de la valeur du prototype
const vehicle3 = new Vehicle('Volkswagen', 'golf', 'bleu');
// __proto__ vaut ici country: ‘France’
vehicle.country = 'Allemagne';
// __proto__ vaut toujours country: ‘France’, MAIS une propriété va être ajoutée à cette instance = country: ‘Allemagne’
DONC si une instance dispose d’une propriété équivalente à celle qui existe sur le constructeur, c’est la valeur de l’instance qui l’emporte !
Ajouter une fonction à un prototype
On peut ajouter du comportement à nos prototypes en ajoutant des fonctions sous forme de méthodes.
// ajout d'une fonction à notre prototype
Vehicle.prototype.honk = function(){
console.log('tuuuuuuuuut');
};
- On voit que le prototype de notre constructeur dispose d’une propriété honk dont la valeur est une fonction.
- Les instances de notre constructeur disposent désormais de cette méthode.
- Depuis le prototype, on peut accéder et modifier chaque propriété des instances du constructeur. Par exemple avec une méthode accelerate() qui augmenterait de 10 la valeur de la propriété speed de nos instances.
// ajout d'une méthode accelerate au prototype de notre constructeur
Vehicle.prototype.accelerate = function(){
this.speed += 10;
};
L’intérêt des prototypes est que cela met à disposition de chaque objet des propriétés dont la valeur peut être des fonctions ou string, number etc…
Occulter une propriété du proto en ajoutant une propriété à un objet
Comme nous l’avons vu précédemment avec l’ajout de la propriété “country” à la golf, il est possible d’ajouter une propriété à un objet et cette valeur prendra le dessus sur celle du prototype de son constructeur.
Il existe un ordre de priorité des propriétés.
// ajout d'une propriété year à notre constructeur
Vehicle.prototype.year = 2017;
Ici, on ajoute une propriété “year” au prototype de l’objet Vehicle, qui est une fonction constructeur, et la valeur de cette propriété est 2017.
Chaque instance de Vehicle aura donc dans son prototype accès à cette propriété et la valeur sera pour chaque instance 2017.
SI on souhaite modifier la valeur de cette propriété uniquement pour une instance, il nous suffit d’ajouter cette propriété directement à l’objet et cette nouvelle propriété, sera prioritaire par rapport à la propriété du prototype.
// modification de la valeur de la propriété year pour l'instance vehicle
vehicle.year = 2016;
On ajoute ici, une propriété directement à l’instance de l’objet Vehicle, pour qu’elle prenne le pas sur la propriété du prototype.
Voici maintenant 2 instances du constructeur Vehicle, dont l’instance qui possède la propriété year dans son propre prototype.
// 2 instances du constructeur vehicle
vehicle {
brand: 'Peugeot',
color: 'red',
model: ‘206’,
speed: 0,
year: 2016
};
vehicle2 {
brand: 'Peugeot',
color: 'red',
model: ‘206’,
speed: 0
};
- vehicle bénéficie de la propriété year dont la valeur est 2016, elle a néanmoins toujours accès dans son prototype à cette même propriété year qui a comme valeur 2017;
- vehicle2 ne possède pas de propriété year directement dans son objet, s’il souhaite accéder à cette propriété, il doit accéder à son prototype, et la valeur est de 2017.
Modifier le prototype d'une fonction après coup
Que se passe-t-il, si on décide de changer le prototype de la fonction constructeur Vehicle pour qu’il pointe sur un nouvel objet ?
Voici notre ancien prototype de Vehicle
// ancien prototype du constructeur Vehicle
console.log(Vehicle.prototype);
// { country: ‘France’; honk: f, year: 2017, accelerate: f }
On va maintenant faire pointer le prototype de notre constructeur Vehicle sur un nouvel objet.
// on fait ici pointer le prototype sur un nouvel objet
Vehicle.prototype = { country: ‘Allemagne’, speedLimitation: fasse };
Nouveau prototype de Vehicle
// Nouveau prototype de Vehicle
console.log(Vehicle.prototype);
// { country: ‘Allemagne’, speedLimitation: false }
Désormais, chaque nouvelle instance du constructeur Vehicle aura comme proto le nouveau prototype.
Consulter le proto d’un objet pour éviter certains pièges
Les prototypes pourront nous éclairer et éviter de tomber dans certains pièges, par exemple vouloir utiliser des fonctions qui ne sont pas mise à disposition par le prototype de cet objet. Voyons un exemple concret.
<body>
<div>azerty</div>
<div>qsdfgh</div>
</body>
<script>
const divs = document.querySelectorAll(‘div’);
console.log(divs)
// NodeList(2) [div, div]
</script>
On voit que divs nous retourne un tableau qui comporte 2 div, on pourrait donc croire qu’on a la possibilité d’effectuer toutes les opérations qu’on peut faire sur un tableau. le problème, c’est que si on observe le proto de divs, on s’aperçoit que c’est un NodeList et non un Array. En observant le prototype, on se rend compte que divs bénéficie de bien moins de méthode qu’un array traditionnel.
On ne pourra donc par exemple pas utiliser la méthode .map() sur cet objet pour le parcourir, étant donné que son prototype n’est pas un array. On ne pourra utiliser uniquement qu’une boucle forEach.
Chaîne d’héritage prototypal
C’est un principe qu’il est important de bien saisir.
// .__proto__
console.log(vehicle.__proto__);
Le proto de l’instance vehicle qui est un objet, hérite du prototype de la fonction constructeur Vehicle.
// .__proto__.__proto___
console.log(vehicle.__proto__.___proto___);
Si on cherche le prototype du prototype, on va se rendre compte que c’est Object. La classe Vehicle hérite implicitement de la classe Object => tout hérite de Object (comme en C#).
// .__proto__.__proto___.__proto__
console.log(vehicle.__proto__.___proto___.__proto__);
Si maintenant, on cherche le prototype de Object, on arrive sur nul. En effet Object en se trouvant à la racine, n’a pas de prototype.
À l’inverse, on peut spécialiser cette chaîne de prototype, par exemple avoir une classe Moto qui hérite de Vehicle, en ajoutant un nouveau prototype à la chaîne prototypale => spécialisation de la chaîne prototypal.
Je souhaite que le constructeur Bike hérite de Vehicle
// Bike hérite de Vehicle
function Bike(brand, model, color, fullPowered, exhaustPipe){
console.log('Dans le constructeur de Bike');
Vehicle.call(this, this.brand, this.model, this.color);
this.fullPowered = fullPowered;
this.exhaustPipe = exhaustPipe;
};
.call() permet d’appeler une fonction en précisant à l’aide du premier argument ce que vous souhaitez que vaille ‘this’.
- Pour que Bike soit considéré comme héritant de Vehicle, il suffit de faire un .call() sur Vehicle et lui passer les paramètres que Vehicle attends quand on créer une nouvelle instance, ne pas oublier le this initial.
Construction de la chaîne prototypal qui fera que Bike héritera du prototype de Vehicle qui hérite lui-même de Object.
// construction de la chaîne prototypale
Bike.prototype = Object.create(Vehicle.prototype);
// on spécifie ici que le prototype de Bike doit être le même que Vehicle
Bike.prototype.constructor = Bike;
// on précise ici que le constructeur c’est Bike
Voilà l’exemple concret d’une chaîne prototypale (ci-dessus)
- Désormais, chaque nouvelles instances de Bike aura comme prototype, le prototype de Bike qui héritera lui même du prototype de Vehicle, qui héritera lui-même du prototype de Object, qui héritera lui même de null.
// création d'une instance de Bike
const zx6r = new Bike('kawasaki', 'ZX-6R', 'rouge', true, 'devil');
instance of
L’opérateur instanceof permet de tester si un objet possède, dans sa chaîne de prototype, la propriété prototype d'un certain constructeur.
On peut maintenant tester notre instance de Bike, zx6r et voir si c’est bien une instance Bike.
// instance of
console.log(zx6r instance of Bike);
// true
console.log(zx6r instance of Vehicle);
// true
console.log(zx6r instance of Object);
// true
Nous sommes bien en présence d’une chaîne d’héritage prototypal.
Les classes ES6
- Nouveau mot clé apparu avec ES6, class permet de créer des classes de façon un peu plus habituelle pour ceux qui sont habitué aux langages orienté objet.
- Il faut juste retenir qu’on est toujours sur une chaîne d’héritage prototypale, mais c’est beaucoup plus “facile” à faire.
- Le mot-clé class pour déterminer qu’elle classe on souhaite construire, quand on souhaite hériter une classe on utilise le mot clé extends et on indique de quelle classe on souhaite hériter. Et dans la classe enfant, on utilise super() pour appeler le constructeur parent.
// class ES6
class Vehicle {
constructor(brand, model, color){
this.brand = brand;
this.model = model;
this.color = color;
}
};
class Bike extends Vehicle {
constructor(brand, model, color, fullPowered, pipeBrand) {
super(brand, model, color);
this.fullPowered = fullPowered;
this.pipeBrand = pipeBrand;
}
};
const zx6r = new Bike('kawasaki', 'Ninja ZX-6R', 'rouge', true, 'devil');
Et voilà, c’est tout pour aujourd’hui, on se retrouve vite pour discuter dans le prochain article, des différentes valeur de this en JavaScript. Ça promet ! See you soon !