Langage avancé Orienté objet
Les types primitifs sont : Undefined
, Null
, Boolean
, String
, Number
et Object
. Il existe également trois types spéciaux : Function
, Array
et RegExp
Les objets#
Contrairement aux variables classiques, les objets sont stockés et copiés par référence
let obj1 = { val: 1 };
let obj2 = obj1;
obj2.val = 2;
console.log(obj1.val); // 2
Pour copier un objet par valeur, plusieurs solutions
let obj1 = { val: 1 };
let obj2 = Object.freeze(obj1); // shallow copy
// ou
let obj2 = JSON.parse(JSON.stringify(obj1)); // nested copy
// ou
let obj2 = structuredClone(obj1); // attention au support
obj2.val = 2;
console.log(obj1.val); // 1
Un objet littéral peut stocker une fonction. Celle-ci peut donc être considérée comme une méthode, et le mot-clé this
au sein de cette fonction fait référence à l’objet lui-même :
let foo = {
bar: 42,
baz: function () {
return this.bar;
}
};
foo.baz(); // 42
Héritage par prototype#
Chaque objet «hérite» des méthodes de son parent. C’est ce que l’on appelle le prototype (visible sous __proto__
). Par exemple :
let foo = {
bar: 42,
baz: function () {
return this.bar;
}
};
// méthode toString() associée à Object.prototype
foo.toString(); // [object Object]
Il est possible de créer un objet à partir du prototype d’un autre objet, et ainsi de créer un héritage :
let child = Object.create(foo);
child.bar = 3.14;
child.baz(); // 3.14
Cet héritage est le seul possible en JavaScript. Les objets héritent donc tous de Object.prototype
, et il est bien entendu possible d’hériter de plusieurs niveaux. C’est ce que l’on appelle la chaine de prototype. Chaque objet ayant la possibilité ou non de redéfinir sa propre méthode (polymorphisme)
let child = Object.create(foo);
child.bar = 3.14;
child.baz = function () {
return this.bar + '!!';
}
child.baz(); // 3.14!!
La classe Object
a une méthode constructor
sur son prototype. Il est donc possible de simuler la création de classe d’objets en la redéfinissant :
let Nombre = {
constructor: function (val) {
this._val = val;
},
get: function () {
return this._val;
}
}
let nb = Object.create(Nombre);
nb.constructor(42);
nb.get(); // 42
Néanmoins, cette solution pose plusieurs soucis dans le cas d’applications complexes. Mais elle est indispensable pour comprendre la suite.
Fonctions#
Lorsque l’on crée une fonction, on crée en fait un Object
classique qui a trois propriétés :
name
le nom de la fonctionlength
le nombre d’argumentsprototype
qui est un objet (Object
) associé à la fonction (pas le prototype lui-même) et qui a donc sa propre méthodeconstructor
(ce qui permet donc de construire des objets).
Cette fonction (objet) n’est pas basée sur Object.prototype
mais sur Function.prototype
. Cela permet notamment d’appeler la fonction avec le mot-clé new
. Ce sera alors la méthode constructor
de l’objet stocké sous prototype
qui sera exécutée. Ainsi, on peut créer des classes d’objets plus facilement :
function Nombre (val) {
this._val = val;
}
let nb = new Nombre(42);
Les méthodes d’objet sont alors à associer à cet objet prototype
:
Nombre.prototype.get = function () {
return this._val;
};
nb.get(); // 42
L’héritage des méthodes peut alors être assez simple, il suffit de copier le prototype de la classe mère (attention, ce n’est pas encore un héritage complet)
function Foo () {}
Foo.prototype.get = function () {};
function Bar () {}
Bar.prototype = Object.create(Foo.prototype);
Comme pour un Object
classique, les fonctions sont stockées et copiées par référence.
Exemple de création de classe Personne
function Personne (nom, age) {
// les attributs
this.nom = nom;
this.age = age;
}
// méthodes
Personne.prototype.affiche = function () {
alert(this.nom + ':' + this.age + ' ans');
}
// création d'un objet
let toto = new Personne("toto", 23);
// appel de la fonction Affiche de cet objet
toto.affiche(); // toto:23ans
toto.nom = "lariflette";
toto.affiche(); // lariflette:23ans
Les classes JS (définies en ES6) ne sont qu’un sucre syntaxique de ce mécanisme.
this
#
Le this
en JavaScript peut prendre plusieurs valeurs, en fonction du contexte. Voici les cas les plus importants :
Dans une fonction appelée simplement
function foo () {
this // window (navigateur) ou global (Node.js)
}
foo();
Une méthode d’objet littéral
let foo = {
bar: function () {
this // l’objet foo
}
}
foo.bar();
Dans une fonction constructeur et sur son prototype / classe
// version Function
function Foo () {
this // l’instance de Foo: f
}
Foo.prototype.bar = function () {
this // l’instance de Foo: f
}
// version Classe
class Foo () {
constructor() {
this // l’instance de Foo: f
}
bar() {
this // l’instance de Foo: f
}
}
let f = new Foo();
f.bar();
Dans un évènement
function foo () {
this // l’objet qui reçoit l’évènement: el
}
el.addEventListener('click', foo);
Changer le contexte du this
#
Dans certains cas, il peut être déroutant de savoir ce que vaut this
. Par exemple :
class Foo {
constructor(el) {
this.a();
el.addEventListener('click', this.event);
}
a() {
this // ? = f
}
event() {
this // ? = el
}
}
let el = document.getElementById('el');
let f = new Foo(el);
Ici, nous sommes dans le cas d’une fonction constructeur, this
est donc l’instance créée : f
. Le constructeur appelle sa méthode a()
, et donc this
au sein de cette méthode vaut l’instance f
. Ensuite, on ajoute un évènement click
, lequel appelle la méthode event
. Le this
au sein de cette méthode ne sera donc plus l’instance, mais bien l’objet qui reçoit l'évènement : el
.
Il est possible de changer le contexte d’exécution du this
lors de l’appel d’une fonction avec sa méthode héritée bind(context)
. Dans le cas précédent, on pourrait donc écrire this.event.bind(this)
pour l’évènement click
:
class Foo {
constructor(el) {
el.addEventListener('click', this.event.bind(this));
}
event() {
this // ? = f
}
}
let f = new Foo(el);
Cela permet de préciser que le this
utilisé dans la méthode event
devra être le this
connu dans le constructeur, et donc l’instance f
.
Encore un cas où cela pourrait être étrange sans explications :
// normal
function Foo () {
// this = foo
setTimeout(function () {
// this = window
}, 100);
tableau.forEach(function () {
// this = window
});
}
// binded
function Foo () {
// this = foo
setTimeout(function () {
// this = foo
}.bind(this), 100);
tableau.forEach(function () {
// this = foo
}.bind(this));
}
// binded avec arrow functions
function Foo () {
setTimeout(() => {
// this = foo
}, 100);
tableau.forEach(() => {
// this = foo
});
}
typeof
/ instanceof
#
L’opérateur typeof
renvoie une chaine qui indique le type de l’objet
typeof 42 === 'number'
typeof 'Hello' === 'string'
typeof true === 'boolean'
typeof undefined === 'undefined'
typeof [0,1,2] === 'object'
typeof function(){} === 'function'
L’opérateur instanceof
vérifie si un objet a, dans sa chaine de prototype, la propriété prototype
d’un certain constructeur
function Foo () {}
let f = new Foo();
f instanceof Foo // true
f instanceof Object // true
function Bar () {}
Bar.prototype = Object.create(Foo.prototype);
let b = new Bar();
b instanceof Bar // true
b instanceof Foo // true
b instanceof Object // true
Exercice - Classes d’objets#
Nous allons créer une interface de dessin de formes très simpliste : des rectangles, ellipses et triangles.
L’interface :
- Créez un formulaire contenant 3 boutons radio pour chaque forme (Rectangle, Ellipse et Triangle). Pensez bien au
<label>
, àname
etvalue
- Créez une zone de dessin (une
div
) - Ajoutez un évènement
click
sur la zone de dessin, et créez un nouvel objet correspondant à la forme souhaitée (case cochée). Passez les coordonnées de la souris (evt.clientX/clientY
), moins la position de la zone de dessin récupérée avecel.getBoundingClientRect()
Tout d’abord, pour les formes rectangles et ellipses :
- Créez la classe
Coordonnee
(x, y) - Créez la classe
Taille
(largeur, hauteur) - Creéz la classe
Couleur
(r, v, b). Cette classe a une méthodegetRVBString()
qui renvoie la couleur au formatrgb(r,v,b)
- Créez la classe
Forme
avec les attributs communs (le constructeur deForme
prends 3 paramètres (coordonnees, taille, couleur) :- un objet
Coordonnee
- un objet
Taille
(générée aléatoirement entre 20 et 100px) - un objet
Couleur
(générée aléatoirement) - un objet DOM
- un objet
- Créez la class
Rectangle
qui hérite deForme
(attributs etprototype
). - Créez la class
Ellipse
qui hérite deForme
(attributs etprototype
). - Implémentez la méthode
draw()
sur l’objetForme
. Cette méthode doit styler l’objet DOM (positionnement absolu, largeur/hauteur, couleur de fond, translation CSS) et l’ajouter dans la zone de dessin - Surclasser la méthode
draw()
deEllipse
pour ajouter des coins arrondis pour générer une ellipse (50%) - Utilisez ces classes pour créer vos formes. La classe
Forme
ne sera jamais instanciée.
Ensuite :
- Créez une méthode
delete()
surForme
pour supprimer une forme déjà créée - Cette méthode doit être appelée lors du click droit (évènement
contextmenu
) sur l’objet DOM associé à la forme
Enfin :
- Créez la classe
Triangle
qui hérite deForme
(attributs etprototype
) - Surclassez la méthode
draw()
pour dessiner un SVG* dans l’objet DOM - Attention, un triangle n’a donc pas de couleur de fond sur l’objet DOM associé (comme c’est le cas sur les rectangles et ellipses)
- Finalisez avec
pointer-events
pour gérer le clic sur la forme uniquement
* SVG envisagé
<svg viewBox="0 0 100 100" fill="..." width="100%" height="100%" preserveAspectRatio="none">
<polygon points="50,0 100,100 0,100">
</svg>