8 mai 2023

Type symbole

Par spĂ©cification, seuls deux types primitifs peuvent servir de clĂ©s de propriĂ©tĂ© d’objet :

  • type string, ou
  • type symbol.

Sinon, si l’on utilise un autre type, tel que nombre, il est automatiquement converti en chaüne de caractùres. Ainsi, obj[1] est identique à obj["1"], et obj[true] est identique à obj["true"].

Jusqu’à prĂ©sent, nous n’utilisions que des chaĂźnes de caractĂšres.

Explorons maintenant les symboles, voyons ce qu’ils peuvent faire pour nous.

Symboles

Un “Symbol” reprĂ©sente un identifiant unique.

Une valeur de ce type peut ĂȘtre créée en utilisant Symbol() :

let id = Symbol();

Lors de la création, nous pouvons donner au symbole une description (également appelée nom de symbole), particuliÚrement utile pour le débogage :

// id est un symbole avec la description "id"
let id = Symbol("id");

Les symboles sont garantis d’ĂȘtre uniques. MĂȘme si nous crĂ©ons beaucoup de symboles avec la mĂȘme description, ce sont des valeurs diffĂ©rentes. La description est juste une Ă©tiquette qui n’affecte rien.

Par exemple, voici deux symboles avec la mĂȘme description – ils ne sont pas Ă©gaux :

let id1 = Symbol("id");
let id2 = Symbol("id");

alert(id1 == id2); // false

Si vous connaissez Ruby ou un autre langage qui comporte Ă©galement une sorte de “symboles”, attention Ă  ne pas vous tromper. Les symboles JavaScript sont diffĂ©rents.

Donc, pour rĂ©sumer, un symbole est une “valeur unique primitive” avec une description facultative. Voyons oĂč nous pouvons les utiliser.

Les symboles ne se convertissent pas automatiquement en chaĂźne de caractĂšres

La plupart des valeurs de JavaScript prennent en charge la conversion implicite en chaßne de caractÚres. Par exemple, nous pouvons alert presque toutes les valeurs et cela fonctionnera. Les symboles sont spéciaux. Ils ne se convertissent pas automatiquement.

Par exemple, cette alert affichera une erreur :

let id = Symbol("id");
alert(id); // TypeError: Impossible de convertir une valeur de symbole en chaĂźne de caractĂšres

C’est un “gardien du langage” contre les erreurs, parce que les chaĂźnes de caractĂšres et les symboles sont fondamentalement diffĂ©rents et ne doivent accidentellement pas ĂȘtre convertis les uns en les autres.

Si nous voulons vraiment afficher un symbole, nous devons appeler .toString() dessus, comme ici :

let id = Symbol("id");
alert(id.toString()); // Symbol(id), maintenant ça marche

Ou récupérer la propriété symbol.description pour afficher la description uniquement :

let id = Symbol("id");
alert(id.description); // id

PropriĂ©tĂ©s “cachĂ©es”

Les symboles nous permettent de crĂ©er des propriĂ©tĂ©s “cachĂ©es” d’un objet, qu’aucune autre partie du code ne peut accĂ©der accidentellement ou Ă©craser.

Par exemple, si nous travaillons avec des objets user qui appartiennent Ă  un code tiers, nous aimerions leur ajouter des identificateurs.

Utilisons une clé symbole pour cela :

let user = { // belongs to another code
  name: "John"
};

let id = Symbol("id");

user[id] = 1;

alert( user[id] ); // nous pouvons accéder aux données en utilisant le symbole comme clé

Quel est l’avantage de l’utilisation de Symbol("id") sur une chaüne de caractùres "id" ?

Poussons un peu plus loin l’exemple pour voir cela.

Comme les objets user appartiennent Ă  une autre base de code, il n’est pas sĂ»r de leur ajouter des champs, car nous pourrions affecter le comportement prĂ©dĂ©fini dans cette autre base de code. Cependant, les symboles ne peuvent pas ĂȘtre accĂ©dĂ©s accidentellement. Le code tiers ne sera pas conscient des symboles nouvellement dĂ©finis, il est donc prudent d’ajouter des symboles aux objets user.

Imaginez qu’un autre script veuille avoir son propre identifiant Ă  l’intĂ©rieur de user, pour sa propre utilisation.

Ensuite, ce script peut créer son propre symbol("id"), comme ceci :

// ...
let id = Symbol("id");

user[id] = "Their id value";

Il n’y aura pas de conflit entre nos identificateurs et les leurs, car les symboles sont toujours diffĂ©rents, mĂȘme s’ils portent le mĂȘme nom.

Notez que si nous utilisions une chaĂźne de caractĂšre "id" au lieu d’un symbole dans le mĂȘme but, il y aurait un conflit :

let user = { name: "John" };

// Notre script utilise la propriété "id"
user.id = "Our id value";

// ...Un autre script veut aussi "id" pour ses besoins 


user.id = "Their id value"
// Boom! écrasé par un autre script!

Symboles dans un objet littéral

Si nous voulons utiliser un symbole dans un objet littéral {...}, nous avons besoin de crochets.

Comme ceci :

let id = Symbol("id");

let user = {
  name: "John",
  [id]: 123 // pas "id": 123
};

C’est parce que nous avons besoin de la valeur de la variable id comme clĂ©, pas de la chaĂźne de caractĂšres “id”.

Les symboles sont ignorés par for
in

Les propriétés symboliques ne participent pas à la boucle for..in.

Par exemple :

let id = Symbol("id");
let user = {
  name: "John",
  age: 30,
  [id]: 123
};

for (let key in user) alert(key); // name, age (pas de symboles)

// l'accĂšs direct par le symbole fonctionne
alert( "Direct: " + user[id] ); // Direct: 123

Object.keys(user) les ignore Ă©galement. Cela fait partie du principe gĂ©nĂ©ral du “dissimulation des propriĂ©tĂ©s symboliques”. Si un autre script ou une bibliothĂšque parcourt notre objet, il n’accĂ©dera pas de maniĂšre inattendue Ă  une propriĂ©tĂ© symbolique.

En revanche, Object.assign copie les propriétés de chaßne de caractÚres et de symbole :

let id = Symbol("id");
let user = {
  [id]: 123
};

let clone = Object.assign({}, user);

alert( clone[id] ); // 123

Il n’y a pas de paradoxe ici. C’est par conception. L’idĂ©e est que lorsque nous clonons un objet ou que nous fusionnons des objets, nous souhaitons gĂ©nĂ©ralement que toutes les propriĂ©tĂ©s soient copiĂ©es (y compris les symboles tels que id).

Symboles globaux

Comme nous l’avons vu, habituellement tous les symboles sont diffĂ©rents, mĂȘme s’ils portent les mĂȘmes noms. Mais parfois, nous voulons que les symboles portant le mĂȘme nom soient les mĂȘmes entitĂ©s. Par exemple, diffĂ©rentes parties de notre application veulent accĂ©der au symbole "id" qui signifie exactement la mĂȘme propriĂ©tĂ©.

Pour cela, il existe un registre de symboles global. Nous pouvons crĂ©er des symboles et y accĂ©der ultĂ©rieurement, ce qui garantit que les accĂšs rĂ©pĂ©tĂ©s portant le mĂȘme nom renvoient exactement le mĂȘme symbole.

Pour lire (crĂ©er en cas d’absence) un symbole du registre, utilisez Symbol.for(key).

Cet appel vĂ©rifie le registre global et, s’il existe un symbole dĂ©crit comme key, le renvoie, sinon il crĂ©e un nouveau symbole Symbol(key) et le stocke dans le registre avec la key donnĂ©e.

Par exemple :

// lit le registre global
let id = Symbol.for("id"); // si le symbole n'existait pas, il est créé

// relit le registre (peut-ĂȘtre Ă  partir d'une autre partie du code)
let idAgain = Symbol.for("id");

// le mĂȘme symbole
alert( id === idAgain ); // true

Les symboles Ă  l’intĂ©rieur de ce registre sont appelĂ©s symboles globaux. Si nous voulons un symbole Ă  l’échelle de l’application, accessible partout dans le code, c’est ce moyen que nous allons utiliser.

Cela ressemble Ă  Ruby

Dans certains langages de programmation, comme Ruby, il existe un seul symbole par nom.

Comme nous pouvons le constater, en JavaScript, c’est vrai pour les symboles globaux.

Symbol.keyFor

Nous avons vu que pour les symboles globaux, Symbol.for(key) renvoie un symbole par son nom. Pour faire le contraire – retourner un nom par symbole global – nous pouvons utiliser : Symbol.keyFor(sym) :

Par exemple :

// get symbol by name
let sym = Symbol.for("name");
let sym2 = Symbol.for("id");

// obtenir le nom par symbole
alert( Symbol.keyFor(sym) ); // name
alert( Symbol.keyFor(sym2) ); // id

Symbol.keyFor utilise en interne le registre de symboles global pour rechercher la clĂ© du symbole. Donc, cela ne fonctionne pas pour les symboles non globaux. Si le symbole n’est pas global, il ne pourra pas le trouver et retournera undefined.

Cela dit, tous les symboles ont la propriété description.

Par exemple :

let globalSymbol = Symbol.for("name");
let localSymbol = Symbol("name");

alert( Symbol.keyFor(globalSymbol) ); // name, global symbol
alert( Symbol.keyFor(localSymbol) ); // undefined, not global

alert( localSymbol.description ); // name

System symbols

Il existe de nombreux “systùmes” symboles que JavaScript utilise en interne et que nous pouvons utiliser pour affiner divers aspects de nos objets.

Ils sont listés dans la documentation Well-known symbols :

  • Symbol.hasInstance
  • Symbol.isConcatSpreadable
  • Symbol.iterator
  • Symbol.toPrimitive
  • Etc.

Par exemple, Symbol.toPrimitive nous permet de dĂ©crire une conversion d’objet en primitive. Nous verrons son utilisation trĂšs bientĂŽt.

Nous nous familiariserons Ă©galement avec d’autres symboles lorsque nous Ă©tudierons les caractĂ©ristiques du langage correspondantes.

Résumé

Symbol est un type primitif pour les identificateurs uniques.

Les symboles sont créés avec l’appel Symbol() ainsi qu’une description facultative.

Les symboles sont toujours de valeurs diffĂ©rentes, mĂȘme s’ils portent le mĂȘme nom. Si nous voulons que les symboles portant le mĂȘme nom soient Ă©gaux, nous devons utiliser le registre global : Symbol.for(key) renvoie (crĂ©e si nĂ©cessaire) un symbole global avec key comme nom. Les multiples appels de Symbol.for avec la mĂȘme key renvoient exactement le mĂȘme symbole.

Les symboles ont deux principaux cas d’utilisation :

  1. PropriĂ©tĂ©s d’objet “masquĂ©es”. Si nous voulons ajouter une propriĂ©tĂ© Ă  un objet qui “appartient” Ă  un autre script ou Ă  une librairie, nous pouvons crĂ©er un symbole et l’utiliser comme clĂ© de propriĂ©tĂ©. Une propriĂ©tĂ© symbolique n’apparait pas dans for..in, elle ne sera donc pas traitĂ©e accidentellement avec d’autres propriĂ©tĂ©s. De plus, elle ne sera pas accessible directement, car un autre script n’a pas notre symbole. Ainsi, la propriĂ©tĂ© sera protĂ©gĂ©e contre une utilisation accidentelle ou un Ă©crasement.

    Ainsi, nous pouvons “dissimuler” quelque chose dans des objets dont nous avons besoin, mais que les autres ne devraient pas voir, en utilisant des propriĂ©tĂ©s symboliques.

  2. De nombreux symboles systÚme utilisés par JavaScript sont accessibles en tant que Symbol.*. Nous pouvons les utiliser pour modifier certains comportements internes. Par exemple, plus tard dans le tutoriel, nous utiliserons Symbol.iterator pour iterables, Symbol.toPrimitive, etc.

Techniquement, les symboles ne sont pas cachĂ©s Ă  100%. Il y a une mĂ©thode intĂ©grĂ©e Object.getOwnPropertySymbols(obj) qui nous permet d’obtenir tous les symboles. Il y a aussi une mĂ©thode nommĂ©e Reflect.ownKeys(obj) qui renvoie toutes les clĂ©s d’un objet, y compris celles symboliques. Donc, ils ne sont pas vraiment cachĂ©s. Mais la plupart des bibliothĂšques, fonctions intĂ©grĂ©es et structures de syntaxe n’utilisent pas ces mĂ©thodes.

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
)