19 octobre 2023

Getters et Setters de propriété

Il y a deux sortes de proriétés d’objet.

Le premier type est les propriétés de données. Nous savons déjà comment travaillez avec. Toutes les propriétés que nous avons utilisés jusqu’à maintenant étaient des propriétés de données.

Le second type de propriété est quelque chose de nouveau. C’est un accesseur de propriété. Ce sont essentiellement des fonctions qui exécutent une récupération ou une déclaration de valeur, mais qui ressemblent à une propriété normale pour le code extérieur.

Getters et Setters

Les accesseurs de propriétés sont représentés par des méthodes “getter” et “setter”. Dans un objet littéral elles se demarquent par get et set :

let obj = {
  get propName() {
    // Getter, le code va récupérer obj.propName
  },

  set propName(value) {
    // Setter, le code va définir obj.propName = value
  }
};

Le getter fonctionne quand obj.propName est lu, le setter – quand il s’agit d’une assignation.

Par exemple, nous avons un objet user avec name et surname :

let user = {
  name: "John",
  surname: "Smith"
};

Maintenant nous voulons ajouter une propriété fullName, qui devrait être "John Smith". Bien sûr, nous ne voulons pas copier-coller l’information existante, donc nous pouvons implémenter un accesseur :

let user = {
  name: "John",
  surname: "Smith",

  get fullName() {
    return `${this.name} ${this.surname}`;
  }
};

alert(user.fullName); // John Smith

De l’extérieur, un accesseur de propriété ressemble à une propriété normale. C’est l’idée d’un accesseur. Nous n’appellons pas user.fullName comme une fonction, nous la lisons normalement : le getter agit en arrière plan.

Pour l’instant, fullName n’a qu’un getter. Si nous essayons d’assigner user.fullName=, il y aura une erreur :

let user = {
  get fullName() {
    return `...`;
  }
};

user.fullName = "Test"; // Erreur (la propriété n'a qu'un getter)

Corrigeons cela en ajoutant un setter pour user.fullName :

let user = {
  name: "John",
  surname: "Smith",

  get fullName() {
    return `${this.name} ${this.surname}`;
  },

  set fullName(value) {
    [this.name, this.surname] = value.split(" ");
  }
};

// Le setter est exécuté avec la valeur donnée.
user.fullName = "Alice Cooper";

alert(user.name); // Alice
alert(user.surname); // Cooper

Comme résultat, nous avons une propriété “virtuelle” fullName. Elle est lisible et ecrivable.

Descripteurs d’accesseur

Les descripteurs d’accesseur de propriété sont différents de ceux pour les propriété de données.

Pour les accesseurs de propriétés, il n’y a pas de value ou writable, à la place il y a les fonctions get et set.

Un descripteur d’accesseur peut avoir :

  • get – une fonction sans arguments, pour la lecture de propriété,
  • set – une fonction avec un argument, qui fonctionne lorsque la propriété change de valeur,
  • enumerable – identique aux propriétés de données
  • configurable – identique aux propriétés de données

Par exemple, pour créer un accesseur fullName avec defineProperty, on peut passer un descripteur avec get et set :

let user = {
  name: "John",
  surname: "Smith"
};

Object.defineProperty(user, 'fullName', {
  get() {
    return `${this.name} ${this.surname}`;
  },

  set(value) {
    [this.name, this.surname] = value.split(" ");
  }
});

alert(user.fullName); // John Smith

for(let key in user) alert(key); // name, surname

Veuillez notez qu’une propriété peut être soit un accesseur (qui a les méthodes get/set) ou une propriété de données (qui a value), pas les deux.

Si nous essayons de fournir les deux get and value dans le même descripteur, il y aura une erreur :

// Erreur : Descripteur de propriété invalide.
Object.defineProperty({}, 'prop', {
  get() {
    return 1
  },

  value: 2
});

Des getters/setters plus intelligents

Les Getters/setters peuvent être utilisés comme des enveloppes autour des “réelles” valeurs de propriété pour gagner plus de contrôles sur leurs opérations.

Par exemple, si nous voulions interdire les noms trop court pour user, nous pourrions avoir un setter name et garder la valeur dans une propriété séparée _name :

let user = {
  get name() {
    return this._name;
  },

  set name(value) {
    if (value.length < 4) {
      alert("Name is too short, need at least 4 characters");
      return;
    }
    this._name = value;
  }
};

user.name = "Pete";
alert(user.name); // Pete

user.name = ""; // Le nom est trop court...

Donc, le nom est stocké dans la propriété _name, et l’accés est fait par le getter et le setter.

Techniquement, le code extérieur est capable d’accéder directement à la propriété en utilisant user._name. Mais il y a une convention très connue, selon laquelle les propriétés commençant par un underscore "_" sont internes et ne devraient pas être touchées depuis l’extérieur des objets.

Utilisation pour la compatibilité

Un des avantages de l’utilisation des accesseurs et qu’ils permettent de prendre le contrôle sur un propriété de données “normale” à tout moment, en la remplaçant par un getter et un setter et modifiant son comportement.

Imaginons que nous commencions des objets utilisateur en utilisant des propriétés de données name et age :

function User(name, age) {
  this.name = name;
  this.age = age;
}

let john = new User("John", 25);

alert( john.age ); // 25

…Mais tôt ou tard, les choses pourraient changer. Au lieu d’age on pourrait decider de stocker birthday, parce que c’est plus précis et plus pratique :

function User(name, birthday) {
  this.name = name;
  this.birthday = birthday;
}

let john = new User("John", new Date(1992, 6, 1));

Maintenant que fait-on avec l’ancien code qui utilise toujours la propriété age ?

On peut essayer de trouver tous les endroits où on utilisent age et les modifier, mais ça prend du temps et ça peut être compliqué à faire si le code est utilisé par plusieurs personnes. En plus, age est une bonne chose à avoir dans user, n’est ce pas ?

Gardons-le.

Ajoutons un getter pour age et résolvons le problème :

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

  // age est calculé à partir de la date actuelle et de birthday
  Object.defineProperty(this, "age", {
    get() {
      let todayYear = new Date().getFullYear();
      return todayYear - this.birthday.getFullYear();
    }
  });
}

let john = new User("John", new Date(1992, 6, 1));

alert( john.birthday ); // birthday est disponible
alert( john.age );      // ...Ainsi que l'age

Maintenant l’ancien code fonctionne toujours et nous avons une propriété additionnelle.

Carte du tutoriel