Aller au contenu

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 fonction
  • length le nombre d’arguments
  • prototype qui est un objet (Object) associé à la fonction (pas le prototype lui-même) et qui a donc sa propre méthode constructor (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 et value
  • 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 avec el.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éthode getRVBString() qui renvoie la couleur au format rgb(r,v,b)
  • Créez la classe Forme avec les attributs communs (le constructeur de Forme 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
  • Créez la class Rectangle qui hérite de Forme (attributs et prototype).
  • Créez la class Ellipse qui hérite de Forme (attributs et prototype).
  • Implémentez la méthode draw() sur l’objet Forme. 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() de Ellipse 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() sur Forme 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 de Forme (attributs et prototype)
  • 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>