ES6 en détails est une série d’articles décrivant les nouvelles fonctionnalités ajoutées au langage de programmation JavaScript avec la sixième édition du standard ECMAScript (ES6 en abrégé).

Que sont les symboles ES6 ?

Les symboles ne sont pas des logos. Ce ne sont pas de petits dessins que vous utilisez dans votre code.

let  =   ♪ × ♫;  // SyntaxError

Il ne s’agit pas non plus d’un procédé littéraire utilisé pour représenter quelque chose d’autre.

Malgré une légère consonance, les symboles ne sont pas non plus des cymbales.

Alors que sont les symboles ?

Rencontre du septième type

Depuis la première standardisation de JavaScript en 1997, il y a toujours eu six types de variables. Jusqu’à ES6, chaque valeur d’un programme écrit en JS rentrait dans l’une de ces catégories :

  • Undefined
  • Null
  • Boolean
  • Number
  • String
  • Object

Chaque type contient un ensemble de valeurs. Les cinq premiers ensembles sont finis. Il y a, bien sûr, seulement deux valeurs booléennes, true et false, on ne va pas en inventer d’autres. Il y a bien plus de valeurs de type Number et String. D’après le standard, il est possible de représenter 18 437 736 874 454 810 627 nombres différents (en incluant NaN, le nombre qui a pour nom « Not a Number », « Pas un Nombre » en français). Ce n’est rien comparé au nombre de chaînes de caractères (String) possible, qui vaut d’après moi (2144,115,188,075,855,872 − 1) ÷ 65,535… mais j’ai peut-être mal compté.

L’ensemble des valeurs pour les objets (type Object) est infini. Chaque objet est unique, un peu comme un flocon de neige. Chaque fois que vous ouvrez une page web, de nombreux objets sont créés.

Les symboles ES6 sont des valeurs, mais ce ne sont pas des chaînes de caractères (String). Ce ne sont pas des objets. C’est une nouveauté : un septième type de valeurs.

Nous allons prendre un exemple pour les étudier.

Un simple petit booléen

Parfois, on aimerait tout simplement pouvoir stocker des données additionnelles dans un objet JavaScript appartenant à quelqu’un d’autre.

Par exemple, supposons que vous écriviez une bibliothèque JS qui utilise les transitions CSS pour déplacer des éléments DOM sur l’écran. Vous avez sans doute remarqué qu’appliquer plusieurs transitions CSS à un seul élément div ne fonctionne pas. C’est moche, ça « saute » sans arrêt. Vous pensez pouvoir arranger ça, mais d’abord vous devez trouver un moyen de savoir si un élément est déjà en mouvement.

Comment y arriver ?

Une solution serait d’utiliser une API CSS pour demander au navigateur si l’élément bouge. Mais cela paraît disproportionné. Votre bibliothèque devrait déjà savoir quel élément se déplace : c’est bien le code de la bibliothèque qui a mis l’élément en mouvement au début !

Ce que vous voulez vraiment, c’est garder une trace des éléments qui bougent. Vous pourriez définir un tableau contenant les éléments qui bougent. Chaque fois que votre bibliothèque anime un élément, vous pourriez alors vérifier si cet élément existe déjà dans le tableau.

Hmm… Cela entraînera donc une recherche linéaire, ce qui peut s’avérer lent si le tableau est long.

En fait, ce que vous voulez vraiment, c’est pouvoir associer un état à l’élément :

if (element.isMoving) {
  smoothAnimations(element);
}
element.isMoving = true;

Cela peut éventuellement poser plusieurs problèmes. Tous ces problèmes reposent sur le fait que votre code n’est pas le seul élément à utiliser le DOM.

  1. Un autre code utilisant for-in ou Objects.keys() pourra trébucher sur la propriété que vous avez créée.
  2. Un concepteur ingénieux travaillant sur la bibliothèque peut avoir déjà pensé à cette technique, ce qui pourra détériorer les interactions avec votre bibliothèque.
  3. Un autre concepteur pourra penser à cela plus tard, ce qui pourra détériorer les interactions avec votre bibliothèque plus tard.
  4. Le comité de standardisation pourrait décider d’ajouter une méthode .isMoving() à tous les éléments, et là vous seriez vraiment mal !

Évidemment, vous pouvez résoudre les trois derniers points en choisissant une chaîne tellement complexe et folle que personne d’autre ne pourra jamais nommer quoi que ce soit de la même façon :

if (element.__$jorendorff_animation_library$NON_PAS_TOUCHE_A_CETTE_PROPRIETE_BISOUS$isMoving__) {
  smoothAnimations(element);
}
element.__$jorendorff_animation_library$NON_PAS_TOUCHE_A_CETTE_PROPRIETE_BISOUS$isMoving__ = true;

Cette solution ne mérite même pas un coup d’œil.

Vous pourriez aussi générer un nom quasiment unique pour cette propriété grâce à la cryptographie :

// obtenir un gloubi-boulga de 1024 caractères Unicode
var isMoving = SecureRandom.generateName();
...
if (element[isMoving]) {
  smoothAnimations(element);
}
element[isMoving] = true;

La syntaxe objet[nom] permet littéralement d’utiliser n’importe quelle chaîne pour dénommer une propriété, ceci fonctionnera et les collisions seront virtuellement impossibles. De plus, votre code est lisible.

En revanche, bonne chance pour le débogage. Chaque fois que vous utiliserez console.log() avec cette propriété, vous obtiendrez une longue chaîne illisible. Et si vous avez besoin de plusieurs propriétés comme celle-ci ? Comment allez-vous les gérer ? Elles auront des noms différents à chaque fois que la page sera rechargée.

Pourquoi est-ce aussi compliqué ? On veut juste un petit booléen !

Tout un symbole

Les symboles sont des valeurs que les programmes peuvent créer et utiliser comme des clés de propriétés sans risquer de rentrer en collision avec les noms déjà utilisés.

var monSymbole = Symbol();

Appeler Symbol() crée un nouveau symbole, c’est-à-dire une valeur qui n’est égale à aucune autre valeur. Un symbole peut être utilisé pour être la clé d’une propriété, comme une chaîne ou un nombre. Étant donné qu’un symbole n’est égal à aucune chaîne, une propriété désignée par un symbole n’entrera pas en collision avec une autre propriété.

obj[monSymbole] = "ok !";       // garanti sans collision
console.log(obj[monSymbole]);   // ok !

Voici comment vous pouvez utiliser un symbole pour la situation dont nous avons discuté ci-avant :

// On crée un symbole unique
var isMoving = Symbol("isMoving");
...
if (element[isMoving]) {
  smoothAnimations(element);
}
element[isMoving] = true;

Quelques notes à propos de ce code :

  • La chaîne "isMoving" dans Symbol("isMoving") est appelée une description. C’est utile lors du débogage. Elle est visible quand on affiche le symbole sur la console via console.log(), quand on le convertit en chaîne avec .toString(), et sans doute aussi dans les messages d’erreurs. C’est tout.
  • element[isMoving] est une propriété dont la clé est un symbole. C’est seulement une propriété dont le nom est un symbole au lieu d’être une chaîne. À part ça, c’est une propriété tout à fait classique.
  • Comme pour les éléments d’un tableau, il est impossible d’accéder aux propriétés dont la clé est un symbole avec la syntaxe littérale utilisant le point (objet.nom). Il faut utiliser les crochets.
  • Il est extrêmement simple d’accéder à une propriété dont la clé est un symbole si vous connaissez le symbole. L’exemple ci-dessus montre comment créer element[isMoving] et y accéder. On pourrait aussi tester if(isMoving in element) voire supprimer element[isMoving] si nécessaire.
  • D’un autre côté, tout ceci n’est possible que si isMoving appartient à la portée. Ainsi les symboles permettent une encapsulation faible : un module qui crée quelques symboles pour lui-même peut les utiliser sur n’importe quel objet, sans craindre les collisions avec des propriétés créées par un autre code.

Les symboles ont été conçus pour éviter les collisions. Pour cette raison, les méthodes classiques pour explorer les objets ignorent les clés qui sont des symboles. La boucle for-in, par exemple, itérera uniquement sur les clés d’un objet qui sont des chaînes de caractères. Les clés qui sont des symboles seront ignorées. Il en va de même pour Object.keys(obj) et Object.getOwnPropertyNames(obj). Cependant, les symboles ne sont pas privés ou invisibles : la nouvelle API, Object.getOwnPropertySymbols(obj), liste les clés d’un objet qui sont des symboles. Une autre API, Reflect.ownKeys(obj), renvoie les clés qui sont des chaînes et des symboles (cette API sera l’objet d’un billet détaillé à venir).

Les différents frameworks et bibliothèques auront vraisemblablement de nombreux cas d’utilisations pour les symboles. Comme nous allons le voir par la suite, le langage lui-même tire parti des symboles pour différents scénarios.

Mais qu’est-ce qu’un symbole ?

> typeof Symbol()
"symbol"

Les symboles ne ressemblent à aucun autre type pré-existant.

Une fois qu’ils sont créés, ils sont immuables. Il est impossible de définir des propriétés sur eux (si vous essayez en mode strict, vous aurez une TypeError). Ils peuvent être utilisés comme noms pour des propriétés. En ce sens, ils ressemblent à des chaînes de caractères.

D’un autre côté, chaque symbole est unique et se distingue des autres symboles (y compris de ceux qui ont la même description) et on peut facilement en créer des nouveaux. En ce sens, ils ressemblent à des objets.

Les symboles ES6 sont semblables aux symboles traditionnels qu’on peut trouver dans les langages tels que Lisp et Ruby. Cependant, ils ne sont pas si profondément ancrés dans les langages. En Lisp, tous les identifiants sont des symboles. En JS, la plupart des identifiants et la plupart des clés restent des chaînes de caractères, les symboles ne sont qu’une option supplémentaire.

Attention, à la différence des symboles dans les autres langages, les symboles JS ne peuvent pas être convertis automatiquement en chaînes de caractères. Si vous essayez de concaténer un symbole et une chaîne de caractères, vous obtiendrez une exception TypeError.

> var sym = Symbol(">3");
> "votre symbole est " + sym
// TypeError: can't convert symbol to string
> `votre symbole est ${sym}`
// TypeError: can't convert symbol to string

Pour éviter cela, on peut convertir le symbole en une chaîne de façon explicite avec String(sym) ou sym.toString().

Trois ensembles de symboles

Il y a trois façons d’obtenir un symbole :

  • Appeler Symbol(). Comme nous l’avons vu avant, cette fonction renvoie un nouveau symbole à chaque fois qu’elle est appelée.
  • Appeler Symbol.for(string). Cela permet d’accéder à un ensemble de symboles existants, appelé registre des symboles. À la différence des symboles uniques définis avec Symbol(), les symboles appartenant au registre sont partagés. Si vous invoquez Symbole.for("chat") trente fois, vous obtiendrez toujours le même symbole à chaque fois. Le registre peut s’avérer utile quand plusieurs pages web, ou plusieurs modules au sein d’une même page web ont besoin de partager un symbole.
  • Utiliser les symboles « connus » définis par le standard tels que Symbol.iterator. Quelques symboles sont définis dans le standard ECMAScript, chacun possède une raison d’être précise.

Si vous avez encore des doutes sur l’utilité des symboles, la dernière catégorie est intéressante car elle illustre comment les symboles ont déjà pu être utilisés en pratique.

Comment ES6 utilise-t-il les symboles connus ?

On a déjà vu un cas utilisation des symboles par ES6 : éviter les collisions avec du code existant. Il y a quelques semaines, dans le billet sur les itérateurs, on a vu que la boucle for (var item of monTableau) démarre en appelant monTableau[Symbol.iterator](). J’ai mentionné que cette méthode aurait pu appeler monTableau.iterator() mais qu’il valait mieux utiliser un symbole pour respecter la rétrocompatibilité.

Maintenant que nous connaissons mieux les symboles, il est plus simple de comprendre pourquoi cela a été fait et ce que ça signifie.

Voici quelques exemples des autres endroits où ES6 utilise les symboles connus (ces fonctionnalités ne sont pas encore implémentées dans Firefox).

  • Rendre instanceof extensible. Avec ES6, l’expression objet instanceof constructeur est définie comme l’appel d’une méthode du constructeur : constructeur[Symbol.hasInstance](objet). Cela signifie qu’on peut étendre instanceof.
  • Éliminer les conflits entre les nouvelles fonctionnalités et du code ancien. On approche ici de la magie noire mais on a découvert que certaines méthodes ES6 pour les tableaux empêchaient certains sites de fonctionner simplement en étant là. D’autres standards du Web ont eu les mêmes problèmes : ajouter de nouvelles méthodes casse les sites existants. Cependant, cette casse est causée par ce qu’on appelle les portées dynamiques. Pour cette raison, ES6 a introduit un symbole spécial : Symbol.unscopables. Celui-ci peut être utilisé par les standard du Web pour éviter que certaines méthodes ne soient impactées par les portées dynamiques.
  • Supporter de nouvelles sortes de correspondances pour les chaînes de caractères. Avec ES5, str.match(monObjet) tentait de convertir monObjet en une RegExp. Avec ES6, le moteur vérifie d’abord si monObjet possède une méthode monObjet[Symbol.match](str). Cela signifie que les bibliothèques peuvent fournir des classes d’analyse de chaînes qui fonctionnent partout où on peut utiliser des objets RegExp.

Chacun de ces cas d’utilisation est relativement restreint et, pour mon code de tous les jours, ça peut être difficile de voir où cela aura un grand impact. Si on prend un peu de recul, c’est plus intéressant : les symboles « connus » de JavaScript peuvent être vus comme une version améliorée des doubles tirets (par exemple __variablePrivée) présents en PHP et Python. Le standard pourra les utiliser à l’avenir pour ajouter de nouveaux éléments au langage, sans risquer de collision avec votre code.

Quand puis-je commencer à utiliser les symboles ES6 ?

Les symboles sont implémentés dans Firefox 36 et Chrome 38. Je les ai moi-même implémentés dans Firefox personnellement, donc si vos symboles se comportent comme des cymbales, vous savez qui contacter.

Pour les navigateurs qui n’ont pas encore implémenté les symboles ES6, vous pouvez utiliser une prothèse telle que core.js. Étant donné que les symboles ne ressemblent pas entièrement à quelque chose d’existant en JS, cette prothèse ne sera pas parfaite. Lisez-bien les mises-en-garde.

La semaine prochaine nous publierons deux billets. D’abord, nous nous intéresserons à des fonctionnalités très attendues qui arrivent enfin en JavaScript avec ES6. Nous démarrerons avec deux fonctionnalités qui datent presque de l’aube de la programmation et nous enchaînerons avec deux autres fonctionnalités, très similaires, mais qui ont été améliorées pour les objets éphémères et les références faibles. Rejoignez-nous la semaine prochaine pour examiner les collections ES6 en détails.

Restez également dans les parages pour un billet supplémentaire de Gastòn Silva sur un sujet qui ne concerne aucune fonctionnalité ES6, mais qui pourrait bien vous fournir le petit coup de pouce nécessaire pour commencer à utiliser ES6 dans vos projets. À bientôt !