Définition de classes ou d'objets ECMAScript

L'utilisation des objets prédéfinis ne constitue qu'une partie des capacités des langages orientés objet, mais ce qui en fait vraiment une force, c'est sa capacité à créer des classes et des objets spécifiques.

ECMAScript possède de nombreux moyens de créer des objets ou des classes.

Méthode d'usine

Méthode originale

Parce que les propriétés des objets peuvent être définies dynamiquement après la création de l'objet, de nombreux développeurs ont écrit du code similaire au suivant lors de l'introduction initiale de JavaScript :

var oCar = new Object;
oCar.color = "blue";
oCar.doors = 4;
oCar.mpg = 25;
oCar.showColor = function() {
  alert(this.color);
};

TIY

Dans le code ci-dessus, l'objet car est créé. Ensuite, plusieurs propriétés lui sont attribuées : sa couleur est bleue, il a quatre portes, et il peut parcourir 25 miles par gallon. La dernière propriété est en fait un pointeur vers une fonction, ce qui signifie que cette propriété est une méthode. Après l'exécution de ce code, l'objet car peut être utilisé.

Cependant, il y a un problème ici, c'est que l'on peut avoir besoin de créer plusieurs instances de car.

Solution : méthode usine

Pour résoudre ce problème, les développeurs ont créé des fonctions usines capables de créer et de retourner des objets de types spécifiques.

Par exemple, la fonction createCar() peut être utilisée pour encapsuler les opérations de création de l'objet car décrites précédemment :

function createCar() {
  var oTempCar = new Object;
  oTempCar.color = "blue";
  oTempCar.doors = 4;
  oTempCar.mpg = 25;
  oTempCar.showColor = function() {
    alert(this.color);
  };
  return oTempCar;
}
var oCar1 = createCar();
var oCar2 = createCar();

TIY

Ici, tous les codes de l'exemple précédent sont inclus dans la fonction createCar(). De plus, il y a une ligne de code supplémentaire qui retourne l'objet car (oTempCar) en tant que valeur de la fonction. Appeler cette fonction crée un nouvel objet et lui attribue toutes les propriétés nécessaires, copiant ainsi l'objet car que nous avons décrit précédemment. De cette manière, nous pouvons facilement créer deux versions d'objets car (oCar1 et oCar2), leurs propriétés étant complètement identiques.

Passer des paramètres à la fonction

Nous pouvons également modifier la fonction createCar(), en lui passant des valeurs par défaut pour chaque propriété, au lieu de simplement affecter des valeurs par défaut aux propriétés :

function createCar(sColor,iDoors,iMpg) {
  var oTempCar = new Object;
  oTempCar.color = sColor;
  oTempCar.doors = iDoors;
  oTempCar.mpg = iMpg;
  oTempCar.showColor = function() {
    alert(this.color);
  };
  return oTempCar;
}
var oCar1 = createCar("red",4,23);
var oCar2 = createCar("blue",3,25);
oCar1.showColor();		//Sortie "red"
oCar2.showColor();		//Sortie "blue"

TIY

Ajouter des arguments à la fonction createCar() permet d'affecter les propriétés color, doors et mpg de l'objet car à créer. Cela rend deux objets avec des propriétés identiques, mais des valeurs de propriétés différentes.

Définir les méthodes de l'objet en dehors de la fonction usine

Bien que ECMAScript devienne de plus en plus formel, les méthodes de création d'objets sont négligées et leur normalisation est toujours opposée. Une partie est due à des raisons sémantiques (elle ne semble pas aussi formelle que l'utilisation de l'opérateur new avec un constructeur de fonction), une autre partie est due à des raisons fonctionnelles. Les raisons fonctionnelles consistent en ce que, de cette manière, il est nécessaire de créer des méthodes pour les objets. Dans les exemples précédents, chaque appel de la fonction createCar() crée une nouvelle fonction showColor(), ce qui signifie que chaque objet a sa propre version de showColor(). En réalité, chaque objet partage la même fonction.

Certains développeurs définissent les méthodes de l'objet en dehors de la fonction d'usine, puis les pointent via une propriété pour éviter ce problème :

function showColor() {
  alert(this.color);
}
function createCar(sColor,iDoors,iMpg) {
  var oTempCar = new Object;
  oTempCar.color = sColor;
  oTempCar.doors = iDoors;
  oTempCar.mpg = iMpg;
  oTempCar.showColor = showColor;
  return oTempCar;
}
var oCar1 = createCar("red",4,23);
var oCar2 = createCar("blue",3,25);
oCar1.showColor();		//Sortie "red"
oCar2.showColor();		//Sortie "blue"

TIY

Dans ce code redécrit ci-dessus, la fonction showColor() est définie avant la fonction createCar(). À l'intérieur de createCar(), un pointeur vers la fonction showColor() déjà existante est attribué à l'objet. En termes de fonctionnalité, cela résout le problème de la création répétée des objets de fonction ; mais en termes de sémantique, cette fonction ne ressemble pas vraiment à une méthode de l'objet.

Toutes ces questions ont généréDéfini par le développeurl'apparition du constructeur.

Méthode de constructeur

La création d'un constructeur est aussi facile que la création d'une fonction d'usine. La première étape consiste à choisir le nom de la classe, c'est-à-dire le nom du constructeur. Selon la convention, le premier caractère de ce nom est en majuscule pour le distinguer des noms de variables généralement en minuscules. À part cela, le constructeur ressemble beaucoup à une fonction d'usine. Considérez l'exemple suivant :

function Car(sColor,iDoors,iMpg) {
  this.color = sColor;
  this.doors = iDoors;
  this.mpg = iMpg;
  this.showColor = function() {
    alert(this.color);
  };
}
var oCar1 = new Car("red",4,23);
var oCar2 = new Car("blue",3,25);

TIY

Voici une explication des différences entre le code et la méthode d'usine. Avant tout, dans le constructeur, on ne crée pas d'objet, mais on utilise le mot-clé this. Lors de l'utilisation de l'opérateur new pour le constructeur, un objet est créé avant l'exécution du premier ligne de code, et c'est uniquement avec this qu'on peut accéder à cet objet. Ensuite, on peut directement attribuer des propriétés à this, par défaut, c'est la valeur de retour du constructeur (il n'est pas nécessaire d'utiliser explicitement l'opérateur return).

Maintenant, la création d'objets en utilisant l'opérateur new et le nom de classe Car ressemble plus à la création d'objets d'objets généraux en ECMAScript.

Vous pourriez demander si ce mode de gestion des fonctions pose les mêmes problèmes que le premier mode. Oui.

Comme les fonctions usine, les constructeurs génèrent des fonctions en répétition, créant une version de fonction indépendante pour chaque objet. Cependant, comme les fonctions usine, on peut également réécrire le constructeur avec une fonction externe, ce qui n'a pas de sens du point de vue sémantique. C'est précisément l'avantage de la méthode prototype que nous allons aborder.

La méthode prototype

Cette méthode utilise l'attribut prototype de l'objet, qu'on peut considérer comme le prototype sur lequel dépend la création de nouveaux objets.

Ici, on utilise d'abord un constructeur vide pour définir le nom de la classe. Ensuite, toutes les propriétés et méthodes sont directement attribuées à l'attribut prototype. Nous reprenons l'exemple précédent, le code suivant :

function Car() {
}
Car.prototype.color = "blue";
Car.prototype.doors = 4;
Car.prototype.mpg = 25;
Car.prototype.showColor = function() {
  alert(this.color);
};
var oCar1 = new Car();
var oCar2 = new Car();

TIY

Dans ce code, on définit d'abord le constructeur (Car), sans aucun code. Les prochaines lignes de code ajoutent des attributs à l'attribut prototype de Car pour définir les attributs de l'objet Car. Lors de l'appel de new Car(), toutes les propriétés du prototype sont immédiatement attribuées à l'objet à créer, ce qui signifie que toutes les instances de Car stockent des pointeurs vers la fonction showColor(). En termes de sens, toutes les propriétés semblent appartenir à un objet, ce qui résout les problèmes des deux premières méthodes.

De plus, en utilisant cette méthode, on peut vérifier le type de l'objet pointé par une variable à l'aide de l'opérateur instanceof. Par conséquent, le code suivant affichera TRUE :

alert(oCar1 instanceof Car); // Affiche "true"

Les problèmes de la méthode prototype

La méthode prototype semble être une bonne solution. Malheureusement, elle n'est pas tout à fait à la hauteur de nos attentes.

D'abord, ce constructeur n'a pas de paramètres. En utilisant la méthode prototype, on ne peut pas initialiser les valeurs des attributs en passant des paramètres au constructeur, car les attributs color de Car1 et Car2 sont tous deux égaux à "blue", les attributs doors sont tous deux égaux à 4, et les attributs mpg sont tous deux égaux à 25. Cela signifie que les valeurs par défaut des attributs doivent être modifiées après la création de l'objet, ce qui est très ennuyeux, mais ce n'est pas tout. Le véritable problème se pose lorsque les attributs pointent vers des objets au lieu de fonctions. Le partage des fonctions ne pose pas de problème, mais les objets sont rarement partagés entre plusieurs instances. Pensez à l'exemple suivant :

function Car() {
}
Car.prototype.color = "blue";
Car.prototype.doors = 4;
Car.prototype.mpg = 25;
Car.prototype.drivers = new Array("Mike","John");
Car.prototype.showColor = function() {
  alert(this.color);
};
var oCar1 = new Car();
var oCar2 = new Car();
oCar1.drivers.push("Bill");
alert(oCar1.drivers);	//Affiche "Mike,John,Bill"
alert(oCar2.drivers);	//Affiche "Mike,John,Bill"

TIY

Dans le code ci-dessus, la propriété drivers est un pointeur vers un objet Array, qui contient deux noms "Mike" et "John". Comme drivers est une valeur de référence, les deux instances de Car pointent vers le même tableau. Cela signifie que si vous ajoutez la valeur "Bill" à oCar1.drivers, cette valeur sera visible dans oCar2.drivers. L'affichage de l'un ou l'autre de ces pointeurs donne le résultat "Mike,John,Bill".

Avec tous ces problèmes lors de la création d'objets, vous devriez penser qu'il existe une méthode raisonnable de création d'objets ? La réponse est oui, il faut utiliser conjointement le constructeur et la méthode prototype.

Méthode hybride constructeur/prototype

L'utilisation conjointe du constructeur et de la méthode prototype permet de créer des objets comme dans d'autres langages de programmation. Ce concept est très simple : utiliser le constructeur pour définir toutes les propriétés non fonctionnelles de l'objet, et utiliser la méthode prototype pour définir les propriétés fonctionnelles (méthodes) de l'objet. En conséquence, toutes les fonctions ne sont créées qu'une seule fois, et chaque objet a son propre exemple d'attribut d'objet.

Nous avons modifié l'exemple précédent, le code est le suivant :

function Car(sColor,iDoors,iMpg) {
  this.color = sColor;
  this.doors = iDoors;
  this.mpg = iMpg;
  this.drivers = new Array("Mike","John");
}
Car.prototype.showColor = function() {
  alert(this.color);
};
var oCar1 = new Car("red",4,23);
var oCar2 = new Car("blue",3,25);
oCar1.drivers.push("Bill");
alert(oCar1.drivers);	//Affiche "Mike,John,Bill"
alert(oCar2.drivers);	//Affiche "Mike,John"

TIY

Il est maintenant plus comme créer un objet commun. Toutes les propriétés non fonctionnelles sont créées dans le constructeur, ce qui signifie que les valeurs par défaut des propriétés peuvent être assignées via les paramètres du constructeur. Comme seule une instance de la fonction showColor() est créée, il n'y a pas de gaspillage de mémoire. De plus, ajouter la valeur "Bill" à l'array drivers de oCar1 n'affecte pas l'array de oCar2, donc lorsque les valeurs des deux arrays sont affichées, oCar1.drivers affiche "Mike,John,Bill", tandis que oCar2.drivers affiche "Mike,John". En utilisant la méthode prototype, instanceof opérateur peut toujours être utilisé pour déterminer le type de l'objet.

Cette méthode est le principal mode d'adoption de ECMAScript, elle possède les caractéristiques des autres méthodes, mais sans leurs effets secondaires. Cependant, certains développeurs pensent toujours que cette méthode n'est pas parfaite.

Méthodes prototypes dynamiques

Pour les développeurs habitués à utiliser d'autres langages, l'utilisation de la méthode de mélange constructeur/pro prototype ne semble pas aussi harmonieuse. Après tout, lors de la définition d'une classe, la plupart des langages orientés objet encapsulent visuellement les attributs et les méthodes. Considérez la classe Java suivante :

class Car {
  public String color = "blue";
  public int doors = 4;
  public int mpg = 25;
  public Car(String color, int doors, int mpg) {
    this.color = color;
    this.doors = doors;
    this.mpg = mpg;
  }
  public void showColor() {
    System.out.println(color);
  }
}

Java a bien emballé toutes les propriétés et méthodes de la classe Car, donc on voit immédiatement ce que ce code doit réaliser, c'est-à-dire définir les informations d'un objet. Les critiques de la méthode de mélange constructeur/pro prototype pensent que chercher les attributs à l'intérieur du constructeur et les méthodes à l'extérieur est un choix logique. C'est pourquoi ils ont conçu la méthode de prototype dynamique pour fournir un style de codage plus convivial.

L'idée fondamentale des méthodes prototypes dynamiques est similaire à celle des constructeurs mélangés/pro prototypes, à savoir définir les attributs non fonctionnels dans le constructeur et les attributs fonctionnels en utilisant les attributs prototypes. La seule différence est l'emplacement d'attribution des méthodes à l'objet. Voici la classe Car réécrite avec les méthodes prototypes dynamiques :

function Car(sColor,iDoors,iMpg) {
  this.color = sColor;
  this.doors = iDoors;
  this.mpg = iMpg;
  this.drivers = new Array("Mike","John");
  if (typeof Car._initialized == "undefined") {
    Car.prototype.showColor = function() {
      alert(this.color);
    };
    Car._initialized = true;
  }
}

TIY

Cette fonction constructeur n'est pas modifiée jusqu'à ce que typeof Car._initialized soit égal à "undefined". Cette ligne de code est la partie la plus importante des méthodes primitives dynamiques. Si cette valeur n'est pas définie, le constructeur continue de définir les méthodes de l'objet de manière prototype et then met Car._initialized à true. Si cette valeur est définie (son valeur est true, typeof retourne Boolean), cette méthode n'est plus créée. En résumé, cette méthode utilise un indicateur (_initialized) pour déterminer si des méthodes ont été attribuées au prototype. Cette méthode est créée et assignée une seule fois, et les développeurs traditionnels en OOP seront ravis de voir que ce code ressemble plus à une définition de classe dans d'autres langages.

Méthode hybride usine

Cette méthode est généralement utilisée comme solution de contournement lorsque la méthode précédente ne peut pas être appliquée. Son objectif est de créer un faux constructeur qui ne renvoie qu'une nouvelle instance d'un autre objet.

Ce code semble très similaire à une fonction usine :

function Car() {
  var oTempCar = new Object;
  oTempCar.color = "blue";
  oTempCar.doors = 4;
  oTempCar.mpg = 25;
  oTempCar.showColor = function() {
    alert(this.color);
  };
  return oTempCar;
}

TIY

Contrairement à la méthode classique, cette méthode utilise l'opérateur new, ce qui lui donne l'apparence d'un véritable constructeur :

var car = new Car();

En raison de l'appel de l'opérateur new à l'intérieur du constructeur Car(), le second opérateur new (situé à l'extérieur du constructeur) sera ignoré, et l'objet créé à l'intérieur du constructeur est renvoyé à la variable car.

Cette méthode a les mêmes problèmes de gestion interne des méthodes d'objet que la méthode classique. Il est fortement recommandé : évitez cette méthode sauf en cas de nécessité absolue.

Quelle méthode choisir

Comme mentionné précédemment, la méthode la plus largement utilisée est la combinaison de constructeurs et de prototypes. De plus, les méthodes primitives dynamiques sont également très populaires et équivalentes en termes de fonctionnalités aux méthodes constructeurs et prototypes. Vous pouvez utiliser l'une de ces deux méthodes. Cependant, n'utilisez pas séparément les méthodes constructeurs ou prototypes classiques, car cela pourrait introduire des problèmes dans le code.

Exemple

Un point intéressant des objets est la manière dont ils résolvent les problèmes. Un des problèmes les plus courants en ECMAScript est la performance de la concaténation des chaînes. Comme dans d'autres langages, les chaînes ECMAScript sont immuables, ce qui signifie que leur valeur ne peut pas être modifiée. Considérez le code suivant :

var str = "hello ";
str += "world";

En réalité, les étapes suivantes sont exécutées à l'arrière-plan par ce code :

  1. Créer une chaîne de caractères pour stocker "hello ";
  2. Créer une chaîne de caractères pour stocker "world".
  3. Créer une chaîne de caractères pour stocker le résultat de la connexion.
  4. Copier le contenu actuel de str dans le résultat.
  5. Copier "world" dans le résultat.
  6. Mettre à jour str pour qu'il pointe vers le résultat.

Chaque fois que la connexion de chaînes de caractères est terminée, les étapes 2 à 6 sont exécutées, ce qui rend cette opération très coûteuse en ressources. Si ce processus est répété plusieurs centaines de fois, voire plusieurs milliers de fois, cela peut entraîner des problèmes de performance. La solution consiste à stocker les chaînes de caractères dans un objet Array, puis à créer la chaîne de caractères finale en utilisant la méthode join() (le paramètre est une chaîne de caractères vide). Imaginez remplacer le code précédent par le suivant :

var arr = new Array();
arr[0] = "hello ";
arr[1] = "world";
var str = arr.join("\"");

De cette manière, il n'y a pas de problème avec l'introduction de plusieurs chaînes de caractères dans l'array, car l'opération de connexion n'a lieu que lors de l'appel de la méthode join(). À ce moment-là, les étapes suivantes sont exécutées :

  1. Créer une chaîne de caractères pour stocker le résultat
  2. Copier chaque chaîne de caractères à la position appropriée dans le résultat

Bien que cette solution soit bonne, il existe une méthode encore meilleure. Le problème est que ce code ne reflète pas exactement son intention. Pour le rendre plus facile à comprendre, vous pouvez emballer cette fonctionnalité dans la classe StringBuffer :

function StringBuffer () {
  this._strings_ = new Array();
}
StringBuffer.prototype.append = function(str) {
  this._strings_.push(str);
};
StringBuffer.prototype.toString = function() {
  return this._strings_.join("\"");
};

Il faut d'abord prêter attention à l'attribut strings dans ce code, qui est un attribut privé par défaut. Il ne possède que deux méthodes, à savoir les méthodes append() et toString(). La méthode append() prend un paramètre et l'ajoute à l'array de chaînes de caractères, tandis que la méthode toString() appelle la méthode join() de l'array pour retourner la chaîne de caractères véritablement connectée. Pour connecter un ensemble de chaînes de caractères avec un objet StringBuffer, vous pouvez utiliser le code suivant :

var buffer = new StringBuffer();
buffer.append("hello ");
buffer.append("world");
var result = buffer.toString();

TIY

Vous pouvez tester les performances de l'objet StringBuffer et des méthodes de concaténation de chaînes traditionnelles avec le code suivant :

var d1 = new Date();
var str = "";
for (var i=0; i < 10000; i++) {
    str += "text";
}
var d2 = new Date();
document.write("Concaténation avec plus: ")
 + (d2.getTime() - d1.getTime()) + " milliseconds");
var buffer = new StringBuffer();
d1 = new Date();
for (var i=0; i < 10000; i++) {
    buffer.append("text");
}
var result = buffer.toString();
d2 = new Date();
document.write("<br />Concaténation avec StringBuffer: ")
 + (d2.getTime() - d1.getTime()) + " milliseconds");

TIY

Ce code effectue deux tests de concaténation de chaînes de caractères, le premier utilise le signe plus, le second utilise la classe StringBuffer. Chaque opération connecte 10000 chaînes de caractères. Les valeurs de date d1 et d2 sont utilisées pour juger du temps nécessaire à l'opération. Notez que lors de la création d'un objet Date sans paramètres, l'objet est assigné la date et l'heure actuelles. Pour calculer le temps que prend l'opération de concaténation, soustrayez simplement la représentation en millisecondes des dates (retournée par la méthode getTime()). C'est une méthode courante pour mesurer les performances de JavaScript. Les résultats de ce test peuvent vous aider à comparer l'efficacité de l'utilisation de la classe StringBuffer avec celle de l'opérateur plus.