26 mai 2023

Attributs et descripteurs de propriétés

Comme nous le savons, les objets peuvent stocker des propriétés.

Jusqu’à présent, une propriété était pour nous une simple paire “clé-valeur”. Mais une propriété d’objet est en réalité une chose plus flexible et plus puissante.

Dans ce chapitre, nous étudierons des options de configuration supplémentaires et, dans le prochain, nous verrons comment les transformer de manière invisible en fonctions de accesseur / mutateur.

Attributs de propriétés

Les propriétés des objets, outre que valeur, ont trois attributs spéciaux (appelés drapeaux, ou “flags” en anglais):

  • writable – si true, la valeur peut être changée, sinon c’est en lecture seule.
  • enumerable – si true, alors listé dans les boucles, sinon non listé.
  • configurable – si true, la propriété peut être supprimée et ces attributs peuvent être modifiés, sinon non.

Nous ne les avons pas encore vues, car généralement elles ne se présentent pas. Lorsque nous créons une propriété “de la manière habituelle”, ils sont tous true. Mais nous pouvons aussi les changer à tout moment.

Voyons d’abord comment obtenir ces “flags”.

La methode Object.getOwnPropertyDescriptor permet d’interroger les informations complètes à propos d’une propriété.

La syntaxe est la suivante :

let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);
obj
L’objet à partir duquel obtenir des informations.
propertyName
Le nom de la propriété.

La valeur renvoyée est un objet dit “descripteur de propriété” : il contient la valeur et tous les descripteurs.

Par exemple :

let user = {
  name: "John"
};

let descriptor = Object.getOwnPropertyDescriptor(user, 'name');

alert( JSON.stringify(descriptor, null, 2 ) );
/* property descriptor:
{
  "value": "John",
  "writable": true,
  "enumerable": true,
  "configurable": true
}
*/

Pour changer les attributs, on peut utiliser Object.defineProperty.

La syntaxe est la suivante :

Object.defineProperty(obj, propertyName, descriptor)
obj, propertyName
L’objet et sa propriété pour appliquer le descripteur.
descriptor
Descripteur de propriété d’objet à appliquer.

Si la propriété existe, defineProperty met à jour ses attributs. Sinon, il crée la propriété avec la valeur et les descripteurs donnés. Dans ce cas, si aucun drapeau n’est fourni, il est supposé false.

Par exemple, ici, une propriété name est créée avec tous les attributs falsy :

let user = {};

Object.defineProperty(user, "name", {
value: "John"
});

let descriptor = Object.getOwnPropertyDescriptor(user, 'name');

alert( JSON.stringify(descriptor, null, 2 ) );
/*
{
"value": "John",
"writable": false,
"enumerable": false,
"configurable": false
}
*/

Comparez-le avec user.name “normalement créé” ci-dessus : maintenant tous les attributs sont falsy. Si ce n’est pas ce que nous voulons, nous ferions mieux de leur attribuer la valeur true dans descriptor.

Voyons maintenant les effets des attributs par exemple.

Lecture seule

Rendons user.name en lecture seule (ne peut pas être réaffecté) en modifiant l’indicateur writeable :

let user = {
name: "John"
};

Object.defineProperty(user, "name", {
writable: false
});

user.name = "Pete"; // Error: Cannot assign to read only property 'name'

Maintenant, personne ne peut changer le nom de notre utilisateur, à moins qu’ils appliquent leur propre defineProperty pour remplacer le nôtre.

Les erreurs apparaissent uniquement en mode strict

En mode non strict, aucune erreur ne se produit lors de l’écriture dans des propriétés non inscriptibles et autres. Mais l’opération ne réussira toujours pas. Les actions violant l’indicateur sont simplement ignorées en silence dans les non-stricts.

Voici le même exemple, mais la propriété est créée à partir de zéro :

let user = { };

Object.defineProperty(user, "name", {
value: "John",
// pour les nouvelles propriétés, nous devons lister explicitement ce qui est vrai
enumerable: true,
configurable: true
});

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

Non énumérable

Ajoutons maintenant un toString personnalisé à user.

Normalement, un toString intégré pour les objets n’est pas énumérable, il n’apparaît pas dans for..in. Mais si nous ajoutons notre propre toString, alors, par défaut, il apparaît dans for..in, comme ceci :

let user = {
name: "John",
toString() {
  return this.name;
}
};

// Par défaut, nos deux propriétés sont répertoriées :
for (let key in user) alert(key); // name, toString

Si nous n’aimons pas cela, alors nous pouvons définir enumerable: false. Ensuite, il n’apparaîtra pas dans la boucle for..in, comme dans la boucle intégrée :

let user = {
name: "John",
toString() {
  return this.name;
}
};

Object.defineProperty(user, "toString", {
enumerable: false
});

// Maintenant notre toString disparaît :
for (let key in user) alert(key); // name

Les propriétés non énumérables sont également exclues de Object.keys :

alert(Object.keys(user)); // name

Non configurable

Le descripteur non configurable (configurable: false) est parfois prédéfini pour les objets et propriétés intégrés.

Une propriété non configurable ne peut pas être supprimée, ses attributs ne peuvent pas être modifiés.

Par exemple, Math.PI est en lecture seule, non énumérable et non configurable :

let descriptor = Object.getOwnPropertyDescriptor(Math, 'PI');

alert( JSON.stringify(descriptor, null, 2 ) );
/*
{
"value": 3.141592653589793,
"writable": false,
"enumerable": false,
"configurable": false
}
*/

Ainsi, un programmeur est incapable de changer la valeur de Math.PI ou de le remplacer.

Math.PI = 3; // Error, because it has writable: false

// supprimer Math.PI ne fonctionnera pas non plus

Nous ne pouvons pas non plus changer Math.PI pour qu’il soit à nouveau writable (éditable) :

// Error, parce que configurable: false
Object.defineProperty(Math, "PI", { writable: true });

Il n’y a absolument rien que nous puissions faire avec Math.PI.

Rendre une propriété non configurable est une voie à sens unique. Nous ne pouvons pas le modifier avec defineProperty.

Veuillez noter : configurable: false empêche les changements d’indicateurs de propriété et sa suppression, tout en permettant de changer sa valeur.

Ici, user.name n’est pas configurable, mais nous pouvons toujours le changer (car il est accessible en écriture) :

let user = {
  name: "John"
};

Object.defineProperty(user, "name", {
  configurable: false
});

user.name = "Pete"; // ça fonctionne
delete user.name; // Error

Ici, nous faisons de user.name une constante “scellée à jamais”, tout comme Math.PI :

let user = {
  name: "John"
};

Object.defineProperty(user, "name", {
  writable: false,
  configurable: false
});

// ne pourra pas changer user.name ou ses indicateurs
// tout cela ne fonctionnera pas :
user.name = "Pete";
delete user.name;
Object.defineProperty(user, "name", { value: "Pete" });
Le seul changement d’attribut possible : writable true → false

Il existe une exception mineure concernant la modification des indicateurs.

Nous pouvons changer writable: true en false pour une propriété non configurable, empêchant ainsi la modification de sa valeur (pour ajouter une autre couche de protection).

Object.defineProperties

Il y a une méthode Object.defineProperties(obj, descriptors) qui permet de définir plusieurs propriétés à la fois.

La syntaxe est la suivante :

Object.defineProperties(obj, {
prop1: descriptor1,
prop2: descriptor2
// ...
});

Par exemple :

Object.defineProperties(user, {
name: { value: "John", writable: false },
surname: { value: "Smith", writable: false },
// ...
});

Nous pouvons donc définir plusieurs propriétés à la fois.

Object.getOwnPropertyDescriptors

Pour obtenir tous les descripteurs de propriété à la fois, nous pouvons utiliser la méthode Object.getOwnPropertyDescriptors(obj).

Avec Object.defineProperties, elle peut être utilisé comme moyen de cloner un objet en tenant compte des attributs :

let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));

Normalement, lorsque nous clonons un objet, nous utilisons une affectation pour copier les propriétés, comme ceci :

for (let key in user) {
clone[key] = user[key]
}

…Mais cela ne copie pas les attributs. Donc, si nous voulons un “meilleur” clone, alors Object.defineProperties est préféré.

Une autre différence est que for..in ignore les propriétés symboliques, mais que Object.getOwnPropertyDescriptors renvoie tous les descripteurs de propriété, y compris ceux symboliques et non énumérables.

Sceller un objet globalement

Les descripteurs de propriété fonctionnent au niveau des propriétés individuelles.

Il existe également des méthodes qui limitent l’accès à l’objet entier :

Object.preventExtensions(obj)
Interdit l’ajout de nouvelles propriétés à l’objet.
Object.seal(obj)
Interdit l’ajout/la suppression de propriétés. Définit configurable: false pour toutes les propriétés existantes.
Object.freeze(obj)
Interdit l’ajout/la suppression/la modification de propriétés. Définit configurable: false, writeable: false pour toutes les propriétés existantes.

Et aussi il y a des tests pour eux :

Object.isExtensible(obj)
Retourne false si l’ajout de propriétés est interdit, sinon true.
Object.isSealed(obj)
Renvoie true si l’ajout/la suppression de propriétés est interdite et que toutes les propriétés existantes ont configurable: false.
Object.isFrozen(obj)
Retourne true si l’ajout/la suppression/la modification de propriétés est interdite et si toutes les propriétés actuelles sont configurable: false, writable: false.

Ces méthodes sont rarement utilisées dans la pratique.

Carte du tutoriel

Commentaires

lire ceci avant de commenter…
  • Si vous avez des améliorations à suggérer, merci de soumettre une issue GitHub ou une pull request au lieu de commenter.
  • Si vous ne comprenez pas quelque chose dans l'article, merci de préciser.
  • Pour insérer quelques bouts de code, utilisez la balise <code>, pour plusieurs lignes – enveloppez-les avec la balise <pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepen…)