La Programmation Orientée Objet est une approche en informatique qui consiste à utiliser les objets pour encapsuler un ensemble de données (des variables) et de traitements (des fonctions).
Les variables d’un objet sont appelées ses propriétés.
Les fonctions d’un objet sont appelées ses méthodes.
Le mot-clé this
agit comme un placeholder qui désigne l’objet ayant appelé la méthode d’un objet.
Autrement dit, si on exécute myCar.getInfo()
, this
vaut myCar
.
var myCar = {
brand : "Toyota",
color : "blue",
numDoors : 4,
getInfo : function() {
return this.color + " " + this.brand + " (" + this.numDoors + " doors)";
}
};
console.log(myCar.getInfo());
Bien souvent, il est nécessaire de créer des objets similaires, qui partagent la même structure — les mêmes méthodes, les mêmes propriétés, mais pas forcemment les même valeurs de propriété: myCar
et yourCar
par exemple. Toutes les voitures ont une marque, une couleur, un nombre de porte, etc, mais pas toutes n’ont pas les mêmes.
Les constructeurs sont des fonctions qui permettent des créer des objets — elles définisssent les propriétés et les méthodes qui appartiendrons à l’objet. On peut considérer un constructeur comme un modèle pour créer de nouveaux objets. Par convention, on nomme les constructeurs avec une majuscule en première lettre pour les distinguer des fonctions qui ne sont pas des constructeurs.
On utilise le mot-clé this
pour définir les valeurs de l’objet.
Le mot-clé new
indique à JavaScript de créer un objet à partir du constructeur, et de d’affecter this
à la variable.
function Car() {
this.brand = "Toyota";
this.color = "blue";
this.numDoor = 4;
}
console.log(new Car()); // { brand: "Toyota", color: "blue", numDoor: 4 }
Pour créer des objets plus facilement, on peut utiliser les paramètres.
function Car(brand, color) {
this.brand = brand;
this.color = color;
this.numDoor = 4;
}
var myCar = new Car("Toyota", "red");
Les objets crée peuvent être utilisés comme n’importe quel objet - on peut accéder aux valeurs, les modifier, en ajouter, invoquer des fonctions, etc.
var myCar = new Car();
myCar.color = "red";
console.log(myCar.color);
Les objets crée via {}
sont dits littéral (litteral object): on a littéralement écrit le contenu de l’objet.
Tandis que pour les objets créés à partir d’un constructeur, on parle d’instance de classe/de constructeur.
Pour que seul la fonction/l’objet ait accès à une variable en lecture et en écriture, on utilise une variable locale au lieu de this
. On dit que la propriété est privée, par opposition à une variable accessible à l’extérieur, qui est dite publique. On préfixe généralement les propriétés privées avec un _
pour les repérer facilement.
Pour donner accès à la variable en lecture uniquement, on peut créer une méthode publique qui retourne la valeur de la variable. On appelle ces méthodes des getter. Leur nom commence généralement par get
.
function Car(brand, color) {
var _brand = brand,
_color = color,
_numDoor = 4;
this.getBrand = function() {
return _brand;
}
this.getColor = function() {
return _color;
}
this.getNumDoor = function() {
return _numDoor;
}
}
var myCar = new Car("toyota", "red");
console.log(myCar.getColor());
Un setter est une méthode qui permet de modifier la valeur de la variable. Elle permet d’effectuer des contrôles sur le type de données accepté, voire des calculs pour mettre à jour d’autres variables. Leur nom commence généralement par set
.
function Person(name, age) {
var _name = name,
_age = age;
this.getName = function() {
return _name;
}
this.getAge = function() {
return _age;
}
this.setAge = function(age) {
if(parseInt(age) != NaN) {
_age = parseInt(age);
}
}
}
La propriété constructor
est automatiquement ajoutée sur les objets crées à partir d’un constructeur, elle contient le constructeur. Pour récupérer le nom du constructeur de l’objet : someObject.constructor.name
. Attention néanmoins, la propriété constructor
peut être réecrite, on ne peut donc pas s’y fier mais l’utiliser pour debugger.
console.log(myCar.constructor);
JavaScript permet de tester si un objet est une instance d’un constructeur donné ou non avec le mot-clé instanceof
.
function Car(brand,color) {
this.brand = brand;
this.color = color;
this.numDoor = 4;
}
var myCar = {brand: "Peugeot", color: "white", numDoor: 2},
myCar2 = new Car("Peugeot", "white");
console.log("myCar:", myCar instanceof Car); // false
console.log("myCar2:", myCar2 instanceof Car); // true
Toutes les propriétés et méthodes d’un objet lui sont propres, il peut redéfinir ses valeurs sans impacter les autres objets. Cela veut également dire qu’une fois créé, l’objet possède une copie de la méthode définie dans le constructeur: si 12 objets sont définis, il y 12 méthodes en mémoire.
Plutôt que de définir les méthodes sur l’objet, on peut les définir sur le prototype
du constructeur. Dans ce cas là, l’objet hérite de la fonction mais ne la possède pas. En modifiant une valeur du prototype, on la modifie pour toutes les instances.
Attention, un prototype
n’a pas accès aux méthodes et propriétés privées du constructeur.
function Car(brand, color) {
var _brand = brand,
_color = color,
_numDoors = 4;
this.getBrand = function() { return _brand; }
this.getColor = function() { return _color; }
this.getNumDoors = function() { return _numDoors; }
}
Car.prototype.getDescription = function() {
return this.getColor() + " " + this.getBrand() + " with " + this.getNumDoors() + " doors";
}
var myCar = new Car("toyota", "red");
console.log(myCar.getDescription()); // red toyota with 4 doors
La propriété __proto__
ou constructor.prototype
retourne le prototype de l’objet.
console.log(myCar.__proto__); // { constructor: function Car(), getDescription: function getDescription() }
console.log(myCar.constructor.prototype);
On peut également utiliser Object.getPrototypeOf()
console.log(Object.getPrototypeOf(myCar));
Le prototype d’un objet est lui-même un objet qui possède des méthodes.
La méthode isPrototypeOf
permet de vérifier si un objet hérite d’un prototype donné ou non.
console.log(Car.prototype.isPrototypeOf(myCar)); // true
La méthode hasOwnProperty
permet de vérifier si une propriété est définie sur l’objet ou si elle est héritée du prototype.
console.log(myCar.hasOwnProperty("getName")); // true
console.log(myCar.hasOwnProperty("getDescription")); // fase
Puisqu’en JavaScript tout est objet, les chaînes de caractère ont également un prototype — et la majorité des données JavaScript ont un prototype. Ainsi pour lister les propriétés d’un objet et non celles de son prototype, on voit très souvent :
for(k in someObject) {
if(someObject.hasOwnProperty(k)) {
console.log(k + ' = ' + someObject[k]);
}
}
On peut récupérer la liste des propriétés d’un prototype avec Object.getOwnPropertyNames(prototype)
var prototype = Object.getPrototypeOf(myCar),
properties = Object.getOwnPropertyNames(prototype);
for(var i=0; i<properties.length; i++) {
console.log(properties[i]);
}
En programmation, on obéit généralement au principe DRY (Do not Repeat Yourself): lorsque du code est répété à plusieurs endroit, les modifications de code (une résolution de bug par exemple) doivent être répercutées à plusieurs endroits — partout où le même morceau de code est utillisé. Cela signifie plus de travail à fournir et plus de risque d’erreurs/oublis.
Quand plusieurs prototypes utilisent une même méthode, utiliser un prototype parent permet de mettre du code en commun et de respecter au mieux le principe DRY. On déclare un prototype générique qui contient les méthodes communes (parent), puis on crée des prototypes enfants via Object.create()
.
Prototype parent:
function Animal() {}
Animal.prototype.describe = function() {
console.log("I'm a " + this.constructor.name);
}
Prototype enfant:
function Bird() {}
Bird.prototype = Object.create(Animal.prototype);
Bird.prototype.constructor = Bird;
var canary = new Bird();
console.log(canary.describe()); // I'm a Bird
function Dog() {}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
var labrador = new Dog();
console.log(labrador.describe()); // I'm a Dog
Le prototype enfant peut ajouter ses propres méthodes, ou redéfinir les méthodes héritées sans impacter les autres prototypes enfants.
Bird.prototype.fly = function() {
console.log("I'm flying !");
}
Dog.prototype.bark = function() {
console.log("I'm barking !");
}
Utiliser un prototype parent est une méthode qui ne fonctionne pas bien pour les objets non liés, comme un oiseau et un avion. Les deux peuvent voler mais ce sont deux objets indépendants qui ne partagent aucune autre caractéristique. Une mixin permet à des objet d’utiliser des fonctions en commun.
Définir une mixin:
var flyMixin = function(obj) {
obj.fly = function() {
console.log("I'm flying !");
}
}
Ajouter une mixin à un prototype:
function Bird() {}
flyMixin(Bird.prototype);
var canary = new Bird();
console.log(canary.fly()); // I'm flying
function Airplane() {}
flyMixin(Airplane.prototype);
var boeing = new Airplane();
console.log(boeing.fly()); // I'm flying
Chainer des méthodes consiste à appeler des méthodes les unes après les autres sur le résultat de la précédente.
Méthodes non chaînées:
monObjet.setName("Name");
monObjet.setFirstName("FirstName");
monObjet.print();
Méthodes chainées:
monObjet.setName("Name")
.setFirstName("FirstName")
.print();
Pour pouvoir chainer des méthodes de cette manière, chaque méthode doit retourner l’objet:
function Person() {
var _name = "",
_firstName = "";
this.setName = function(name) {
_name = name;
return this;
}
this.setFirstName = function(firstName) {
_firstName = firstName;
return this;
}
this.print = function() {
console.log(_firstName + " " + _name);
return this;
}
}
La méthode (publique) toString
est une méthode spéciale qui retourne la valeur de l’objet lorsqu’on le caste en chaîne de caractère.
function Car(brand,color) {
this.brand = brand;
this.color = color;
this.numDoor = 4;
}
var myCar = new Car("toyota", "red");
console.log(""+myCar); // [object Object]
function Car(brand,color) {
this.brand = brand;
this.color = color;
this.numDoor = 4;
this.toString = function() {
return "[" + this.color + " " + this.brand + " Car]";
}
}
var myCar = new Car("toyota", "red");
console.log(""+myCar); // [red toyota Car]
Depuis ES6, un objet peut implémenter @@Symbol.toPrimitive
pour contrôler la valeur de l’objet lorsqu’on le caste en une valeur primitive.
function Example() {
this[Symbol.toPrimitive] = function(hint) {
switch(hint) {
case "string": return "Hello";
case "number": return 42;
// when pushed, most classes (except Date)
// default to returning a number primitive
default: return 0;
}
}
};
var obj = new Example();
console.log(Number(obj)); // 42
console.log(String(obj)); // "Hello"
console.log(0 + obj); // 0
console.log("" + obj); // "0"
Lors de l’exécution d’une méthode:
lorsque la fonction d’un objet est appelée, this
est l’objet appelant
var someObject = {
name: "Example",
someMethod: function() {
console.log(this); // { name: "Example", someMethod: someMethod() }
}
}
someObject.someMethod();
Lors d’une instanciation:
lorsqu’on appelle le mot-clé new
pour appeler une fonction, this
designe l’objet créé
function Person(name, age) {
this.name = name;
this.age = age;
console.log(this); // { name: 'Bob', age: 20 }
}
const user = new Person('Bob', 20);
Lors de l’exécution d’une fonction sans objet:
this
est l’objet de plus au niveau (dans un navigateur, il s’agit de l’objet window
).
function someFunction() {
console.log(this); // Window object
}
someFunction();
Lorsqu’on exécute une fonction dans un constructeur, il donc faut faire attention à ne pas perdre this
.
Pour ce faire, on peut
utiliser une variable à laquelle on a affecté this
:
function Person(name, age) {
var self = this;
this.name = name;
this.age = age;
(function() {
console.log(this); // Window object
console.log(self); // {name: 'Bob', age: 20}
})();
}
var user = new Person('Bob', 20);
utiliser la fonction flèche (depuis ES6)
Le this
à l’intérieur de la fonction flèche est le this
qui existait au moment où a été déclaré la fonction
function Person(name, age) {
var self = this;
this.name = name;
this.age = age;
(() => {
console.log(this); // {name: 'Bob', age: 20}
console.log(self); // {name: 'Bob', age: 20}
})();
}
var user = new Person('Bob', 20);
attacher this
(en utilisant call
, apply
ou bind
)
function Person(name, age) {
var self = this;
this.name = name;
this.age = age;
(function() {
console.log(this); // {name: 'Bob', age: 20}
console.log(self); // {name: 'Bob', age: 20}
}).call(this);
}
var user = new Person('Bob', 20);
Il existe trois méthodes permettant de modifier la valeur de this
bind
retourne une fonction partielle, qui peut être utilisée en callback
var fct = mafonction.bind(obj, 'arg1', 'arg1');
fct();
call
appelle la fonction immédiatement
mafonction.call(obj, 'arg1', 'arg1');
apply
appelle la fonction immédiatement mais prend les arguments dans un tableau
(Call for Comma, Apply for Array)
mafonction.apply(obj, ['arg1', 'arg1']);
Des modules sont des objets qui mettent à disposition des fonctions, mais qui ne peuvent pas être instanciés.
Ils permettent de rassembler un ensemble de fonctions sous un même espace de nom.
Exemple:
var k = Math.pow(2, 8);
Dans un navigateur, certains modules sont préchargés.
On peut créer méthodes non pas sur le prototype
du constructeur, mais directement sur le constructeur, c’est ce qu’on appelle une méthode statique. Une méthode statique permet de mettre à disposition des méthodes liées à l’objet mais qui ne s’appliquent pas l’objet — un peu sur le même principe qu’un module.
this
est indéfini à l’intérieur d’une méthode statique.function Point(x, y) {
this.x = x;
this.y = y;
}
Point.distance = function(p1, p2) {
var dx = p1.x - p2.x,
dy = p1.y - p2.y;
return Math.hypot(dx, dy);
}
var p1 = new Point(5, 5);
var p2 = new Point(10, 10);
console.log(Point.distance(p1, p2)); // 7.0710678118654755
console.log(p1.distance(p1, p2)); // TypeError: p1.distance is not a function