5 mai 2023

Le constructeur, l'opérateur "new"

La syntaxe normale {...} permet de créer un seul objet. Mais souvent, nous devons créer de nombreux objets similaires, tels que plusieurs utilisateurs ou éléments de menu, etc.

Cela peut être fait en utilisant les fonctions constructeur et l’opérateur "new".

La function constructeur

Les fonctions constructeur sont techniquement des fonctions habituelles. Il existe cependant deux conventions :

  1. Elles sont nommées avec une lettre majuscule en premier.
  2. Elles ne devraient être executées qu’avec l’opérateur "new".

Par exemple :

function User(name) {
  this.name = name;
  this.isAdmin = false;
}

let user = new User("Jack");

alert(user.name); // Jack
alert(user.isAdmin); // false

Quand une fonction est exécutée avec new, elle effectue les étapes suivantes :

  1. Un nouvel objet vide est créé et affecté à this.
  2. Le corps de la fonction est exécuté. Habituellement, il modifie this, y ajoutant de nouvelles propriétés.
  3. La valeur de this est retournée.

En d’autres termes, new User(...) fait quelque chose comme :

function User(name) {
  // this = {};  (implicitement)

  // ajoute des propriétés à this
  this.name = name;
  this.isAdmin = false;

  // return this;  (implicitement)
}

Donc let user = new User("Jack") donne le même résultat que :

let user = {
  name: "Jack",
  isAdmin: false
};

Maintenant, si nous voulons créer d’autres utilisateurs, nous pouvons appeler new User("Ann"), new User("Alice") etc. Beaucoup plus court que d’écrire littéralement à chaque fois, et aussi facile à lire.

C’est l’objectif principal des constructeurs – implémenter du code de création d’objet réutilisable.

Notons encore une fois – techniquement, n’importe quelle fonction (à l’exception des fonctions fléchées, car elles n’ont pas de this) peut être utilisée comme constructeur. Elle peut être exécutée avec new, et elle exécutera l’algorithme ci-dessus. La “première lettre majuscule” est une convention, pour indiquer clairement qu’une fonction doit être exécutée avec new.

new function() { … }

Si nous avons beaucoup de lignes de code concernant la création d’un seul objet complexe, nous pouvons les envelopper dans une fonction constructeur, comme ceci :

// create a function and immediately call it with new
let user = new function() {
  this.name = "John";
  this.isAdmin = false;

  // ...autre code pour la création d'utilisateur
  // peut-être une logique complexe et des déclarations
  // de variables locales etc.
};

Ce constructeur ne peut pas être appelé à nouveau, car il n’est enregistré nulle part, juste créé et appelé. Cette astuce vise donc à encapsuler le code qui construit l’objet unique, sans réutilisation future.

Constructeur mode test : new.target

Trucs avancés

La syntaxe de cette section est rarement utilisée, sautez-la à moins de vouloir tout savoir.

Dans une fonction, nous pouvons vérifier si elle a été appelée avec new ou sans, en utilisant la propriété spéciale new.target.

Elle n’est pas définie pour les appels réguliers et équivaut à la fonction si elle est appelée avec new :

function User() {
  alert(new.target);
}

// sans "new":
User(); // undefined

// avec "new":
new User(); // function User { ... }

Cela peut être utilisé dans la fonction pour savoir si elle a été appelée avec new, “en mode constructeur”, ou sans “en mode normal”.

Nous pouvons également faire des appels new et réguliers pour faire la même chose, comme ceci :

function User(name) {
  if (!new.target) { // si vous m'executer sans new
    return new User(name); // ...j'ajouterai un new pour vous
  }

  this.name = name;
}

let john = User("John"); // redirige l'appel vers un new User
alert(john.name); // John

Cette approche est parfois utilisée dans les librairies pour rendre la syntaxe plus flexible. Pour que les gens puissent appeler la fonction avec ou sans new, et que cela fonctionne toujours.

Ce n’est probablement pas une bonne chose à utiliser partout cependant, car l’omission de new rend un peu moins évident ce qui se passe. Avec new, nous savons tous que le nouvel objet est en cours de création.

Retour des constructeurs

Généralement, les constructeurs n’ont pas d’instruction return. Leur tâche consiste à écrire tous les éléments nécessaires dans this, qui devient automatiquement le résultat.

Mais s’il y a une déclaration return, alors la règle est simple :

  • Si return est appelé avec un object, alors il est renvoyé à la place de this.
  • Si return est appelé avec une primitive, elle est ignorée.

En d’autres termes, return avec un objet renvoie cet objet, dans tous les autres cas, this est renvoyé.

Par exemple, ici return remplace this en retournant un objet :

function BigUser() {

  this.name = "John";

  return { name: "Godzilla" };  // <-- retourne cet objet
}

alert( new BigUser().name );  // Godzilla, obtenu cet objet

Et voici un exemple avec un return vide (ou nous pourrions placer une primitive après, peu importe) :

function SmallUser() {

  this.name = "John";

  return; // renvoie this
}

alert( new SmallUser().name );  // John

Généralement, les constructeurs n’ont pas d’instruction return. Nous mentionnons ici le comportement spécial avec les objets renvoyés principalement dans un souci de complétude.

Omettre les parenthèses

À propos, on peut omettre les parenthèses après new :

let user = new User; // <-- pas de parenthèses
// identique à
let user = new User();

L’omission de parenthèses ici n’est pas considérée comme un “bon style”, mais la syntaxe est autorisée par la spécification.

Les méthodes dans les constructeurs

L’utilisation de fonctions de constructeur pour créer des objets offre une grande flexibilité. La fonction constructeur peut avoir des paramètres qui définissent comment construire l’objet et ce qu’il doit y mettre.

Bien sûr, nous pouvons ajouter à this non seulement des propriétés, mais également des méthodes.

Par exemple, new User(name) ci-dessous créer un objet avec le name donné et la méthode sayHi :

function User(name) {
  this.name = name;

  this.sayHi = function() {
    alert( "My name is: " + this.name );
  };
}

let john = new User("John");

john.sayHi(); // My name is: John

/*
john = {
   name: "John",
   sayHi: function() { ... }
}
*/

Pour créer des objets complexes, il existe une syntaxe plus avancée, les classes, que nous allons couvrir plus tard.

Résumé

  • Les fonctions constructeur ou, plus brièvement, les constructeurs, sont des fonctions normales, mais il est généralement convenu de les nommer avec une première lettre en majuscule.
  • Les fonctions constructeur ne doivent être appelées qu’avec new. Un tel appel implique la création d’un objet this vide au début de la fonction et le renvoi de l’objet complété à la fin.

Nous pouvons utiliser des fonctions constructeurs pour créer plusieurs objets similaires.

JavaScript fournit des fonctions constructeur pour de nombreux objets intégrés du langage : comme Date pour les dates, Set pour les ensembles et d’autres que nous prévoyons d’étudier.

Objets, nous reviendrons !

Dans ce chapitre, nous ne couvrons que les bases sur les objets et les constructeurs. Elles sont essentielles pour en savoir plus sur les types de données et les fonctions dans les chapitres suivants.

Après avoir appris cela, nous reviendrons aux objets et les couvrirons en profondeur dans les chapitres Prototypes, héritage et Classes.

Exercices

importance: 2

Est-il possible de créer des fonctions A et B tel que new A() == new B() ?

function A() { ... }
function B() { ... }

let a = new A();
let b = new B();

alert( a == b ); // true

Si c’est le cas, donnez un exemple de leur code.

Oui c’est possible.

Si une fonction retourne un objet alors new le retourne au lieu de this.

Ainsi, ils peuvent, par exemple, renvoyer le même objet défini en externe obj :

let obj = {};

function A() { return obj; }
function B() { return obj; }

alert( new A() == new B() ); // true
importance: 5

Créer une fonction constructeur Calculator qui crée des objets avec 3 méthodes :

  • read() demande deux valeurs et les enregistre en tant que propriétés d’objet avec les noms a et b respectivement.
  • sum() renvoie la somme de ces propriétés.
  • mul() renvoie le produit de la multiplication de ces propriétés.

Par exemple :

let calculator = new Calculator();
calculator.read();

alert( "Sum=" + calculator.sum() );
alert( "Mul=" + calculator.mul() );

Exécuter la démo

Open a sandbox with tests.

function Calculator() {

  this.read = function() {
    this.a = +prompt('a?', 0);
    this.b = +prompt('b?', 0);
  };

  this.sum = function() {
    return this.a + this.b;
  };

  this.mul = function() {
    return this.a * this.b;
  };
}

let calculator = new Calculator();
calculator.read();

alert( "Sum=" + calculator.sum() );
alert( "Mul=" + calculator.mul() );

Ouvrez la solution avec des tests dans une sandbox.

importance: 5

Créer une fonction constructeur Accumulator(startingValue).

L’objet qu’il crée devrait :

  • Stocker la “valeur actuelle” dans la propriété value. La valeur de départ est définie sur l’argument du constructeur startingValue.
  • La méthode read() devrait utiliser prompt pour lire un nouveau numéro et l’ajouter à value.

En d’autres termes, la propriété value est la somme de toutes les valeurs entrées par l’utilisateur avec la valeur initiale startingValue.

Voici la démo du code :

let accumulator = new Accumulator(1); // valeur initiale 1

accumulator.read(); // ajoute la valeur entrée par l'utilisateur
accumulator.read(); // ajoute la valeur entrée par l'utilisateur

alert(accumulator.value); // affiche la somme de ces valeurs

Exécuter la démo

Open a sandbox with tests.

function Accumulator(startingValue) {
  this.value = startingValue;

  this.read = function() {
    this.value += +prompt('How much to add?', 0);
  };

}

let accumulator = new Accumulator(1);
accumulator.read();
accumulator.read();
alert(accumulator.value);

Ouvrez la solution avec des tests dans une sandbox.

Carte du tutoriel