Caractéristiques de l'utilisation du type de données Symbol en JavaScript

Les primitives de caractères sont l'une des innovations de la norme ES6, qui a apporté de précieuses fonctionnalités à JavaScript. Les symboles représentés par le type de données Symbol sont particulièrement utiles lorsqu'ils sont utilisés comme identificateurs pour les propriétés d'objet. Dans le cadre d'un tel scénario de leur application, la question se pose de savoir ce qu'ils peuvent, ce que les lignes ne peuvent pas.



Dans le document dont nous publions la traduction aujourd'hui, nous parlerons du type de données Symbol en JavaScript. Nous allons commencer par passer en revue certaines des fonctionnalités JavaScript dont vous avez besoin pour naviguer afin de gérer les symboles.

Informations préliminaires


En JavaScript, en fait, il existe deux types de valeurs. Le premier type - valeurs primitives, le second - objet (ils incluent également des fonctions). Les valeurs primitives incluent des types de données simples comme les nombres (cela comprend tout, des nombres entiers aux nombres à virgule flottante, les valeurs Infinity et NaN ), les valeurs logiques, les chaînes, null valeurs undefined et null . Notez que lors de la vérification de typeof null === 'object' donne true , null est une valeur primitive.

Les valeurs primitives sont immuables. Ils ne peuvent pas être modifiés. Bien sûr, vous pouvez écrire quelque chose de nouveau dans une variable stockant une valeur primitive. Par exemple, cela écrit une nouvelle valeur dans la variable x :

 let x = 1; x++; 

Mais en même temps, il n'y a pas de changement (mutation) de la valeur numérique primitive 1 .

Dans certains langages, par exemple en C, il existe des concepts de passage d'arguments de fonctions par référence et par valeur. JavaScript a également quelque chose de similaire. L'organisation exacte du travail avec les données dépend de leur type. Si une valeur primitive représentée par une certaine variable est transmise à la fonction, puis modifiée dans cette fonction, la valeur stockée dans la variable d'origine ne change pas. Cependant, si vous transmettez la valeur d'objet représentée par la variable à la fonction et la modifiez, ce qui est stocké dans cette variable changera également.

Prenons l'exemple suivant:

 function primitiveMutator(val) { val = val + 1; } let x = 1; primitiveMutator(x); console.log(x); // 1 function objectMutator(val) { val.prop = val.prop + 1; } let obj = { prop: 1 }; objectMutator(obj); console.log(obj.prop); // 2 

Les valeurs primitives (à l'exception du mystérieux NaN , qui n'est pas égal à lui-même) se révèlent toujours égales à d'autres valeurs primitives qui se ressemblent. Par exemple:

 const first = "abc" + "def"; const second = "ab" + "cd" + "ef"; console.log(first === second); // true 

Cependant, la construction de valeurs d'objets qui se ressemblent extérieurement ne conduira pas au fait que les entités seront obtenues, lorsque comparées, leur égalité les unes aux autres sera révélée. Vous pouvez le vérifier en:

 const obj1 = { name: "Intrinsic" }; const obj2 = { name: "Intrinsic" }; console.log(obj1 === obj2); // false //     .name   : console.log(obj1.name === obj2.name); // true 

Les objets jouent un rôle fondamental dans JavaScript. Ils sont utilisés littéralement partout. Par exemple, ils sont souvent utilisés sous la forme de collections de clés / valeurs. Mais avant l'avènement du type de données Symbol , seules les chaînes pouvaient être utilisées comme clés d'objet. Il s'agissait d'une grave limitation de l'utilisation des objets sous forme de collections. Lorsque vous essayez d'affecter une valeur non chaîne en tant que clé d'objet, cette valeur a été convertie en chaîne. Vous pouvez le vérifier en:

 const obj = {}; obj.foo = 'foo'; obj['bar'] = 'bar'; obj[2] = 2; obj[{}] = 'someobj'; console.log(obj); // { '2': 2, foo: 'foo', bar: 'bar',    '[object Object]': 'someobj' } 

Soit dit en passant, bien que cela nous éloigne un peu du sujet des caractères, je tiens à noter que la structure des données de la Map été créée pour permettre l'utilisation de magasins de données de clé / valeur dans les situations où la clé n'est pas une chaîne.

Qu'est-ce qu'un symbole?


Maintenant que nous avons compris les caractéristiques des valeurs primitives en JavaScript, nous sommes enfin prêts à commencer à parler des caractères. Un symbole est une signification primitive unique. Si vous vous approchez des symboles à partir de cette position, vous remarquerez que les symboles à cet égard sont similaires aux objets, car la création de plusieurs instances des symboles entraînera la création de valeurs différentes. Mais les symboles sont d'ailleurs des valeurs primitives immuables. Voici un exemple de travail avec des personnages:

 const s1 = Symbol(); const s2 = Symbol(); console.log(s1 === s2); // false 

Lors de la création d'une instance d'un caractère, vous pouvez utiliser le premier argument de chaîne facultatif. Cet argument est une description du symbole destiné à être utilisé dans le débogage. Cette valeur n'affecte pas le symbole lui-même.

 const s1 = Symbol('debug'); const str = 'debug'; const s2 = Symbol('xxyy'); console.log(s1 === str); // false console.log(s1 === s2); // false console.log(s1); // Symbol(debug) 

Les symboles comme clés de propriété des objets


Les symboles peuvent être utilisés comme clés de propriété pour les objets. C'est très important. Voici un exemple de leur utilisation en tant que telle:

 const obj = {}; const sym = Symbol(); obj[sym] = 'foo'; obj.bar = 'bar'; console.log(obj); // { bar: 'bar' } console.log(sym in obj); // true console.log(obj[sym]); // foo console.log(Object.keys(obj)); // ['bar'] 

Veuillez noter que les clés spécifiées par des caractères ne sont pas renvoyées lorsque la méthode Object.keys() est Object.keys() . Le code écrit avant l'apparition des caractères dans JS ne sait rien d'eux, par conséquent, les informations sur les clés des objets représentés par des caractères ne doivent pas être retournées par l'ancienne méthode Object.keys() .

À première vue, il peut sembler que les caractéristiques des personnages ci-dessus vous permettent de les utiliser pour créer des propriétés privées d'objets JS. Dans de nombreux autres langages de programmation, vous pouvez créer des propriétés d'objets cachés à l'aide de classes. L'absence de cette fonctionnalité a longtemps été considérée comme l'une des lacunes de JavaScript.

Malheureusement, le code qui fonctionne avec les objets peut accéder librement à leurs clés de chaîne. Le code peut également accéder à des clés spécifiées par des caractères, même si le code à partir duquel ils travaillent avec l'objet n'a pas accès au caractère correspondant. Par exemple, en utilisant la méthode Reflect.ownKeys() , vous pouvez obtenir une liste de toutes les clés d'un objet, à la fois celles qui sont des chaînes et celles qui sont des caractères:

 function tryToAddPrivate(o) { o[Symbol('Pseudo Private')] = 42; } const obj = { prop: 'hello' }; tryToAddPrivate(obj); console.log(Reflect.ownKeys(obj));       // [ 'prop', Symbol(Pseudo Private) ] console.log(obj[Reflect.ownKeys(obj)[1]]); // 42 

Notez que des travaux sont en cours pour équiper les classes avec la possibilité d'utiliser des propriétés privées. Cette fonctionnalité est appelée Champs privés . Certes, il n'affecte pas absolument tous les objets, se référant uniquement à ceux d'entre eux qui sont créés sur la base de classes préalablement préparées. La prise en charge des champs privés est déjà disponible dans la version 72 du navigateur Chrome et les versions antérieures.

Empêcher les collisions de noms de propriétés d'objets


Bien entendu, les symboles n'ajoutent pas à JavaScript la possibilité de créer des propriétés privées d'objets, mais ils constituent une innovation précieuse dans le langage pour d'autres raisons. À savoir, ils sont utiles dans des situations où certaines bibliothèques doivent ajouter des propriétés aux objets décrits en dehors d'eux, et en même temps ne pas avoir peur d'une collision des noms de propriétés des objets.

Prenons un exemple dans lequel deux bibliothèques différentes souhaitent ajouter des métadonnées à un objet. Il est possible que les deux bibliothèques doivent équiper l'objet de certains identifiants. Si vous utilisez simplement quelque chose comme une chaîne id de deux lettres pour le nom d'une telle propriété, vous pouvez rencontrer une situation où une bibliothèque écrase la propriété spécifiée par l'autre.

 function lib1tag(obj) { obj.id = 42; } function lib2tag(obj) { obj.id = 369; } 

Si nous utilisons les symboles dans notre exemple, chaque bibliothèque peut générer, lors de l'initialisation, les symboles dont elle a besoin. Ces symboles peuvent ensuite être utilisés pour affecter des propriétés aux objets et accéder à ces propriétés.

 const library1property = Symbol('lib1'); function lib1tag(obj) { obj[library1property] = 42; } const library2property = Symbol('lib2'); function lib2tag(obj) { obj[library2property] = 369; } 

C'est en regardant un tel scénario que vous pouvez bénéficier de l'apparition de caractères en JavaScript.

Cependant, cela peut soulever une question concernant l'utilisation des bibliothèques pour les noms des propriétés des objets, des chaînes aléatoires ou des chaînes avec une structure complexe, y compris, par exemple, le nom de la bibliothèque. Des chaînes similaires peuvent former quelque chose comme des espaces de noms pour les identifiants utilisés par les bibliothèques. Par exemple, cela pourrait ressembler à ceci:

 const library1property = uuid(); //       function lib1tag(obj) { obj[library1property] = 42; } const library2property = 'LIB2-NAMESPACE-id'; //     function lib2tag(obj) { obj[library2property] = 369; } 

En général, vous pouvez le faire. En fait, des approches similaires sont très similaires à ce qui se passe lors de l'utilisation de symboles. Et si, en utilisant des identifiants ou des espaces de noms aléatoires, quelques bibliothèques ne génèrent pas, par hasard, les mêmes noms de propriété, alors il n'y aura aucun problème avec les noms.

Un lecteur avisé dirait maintenant que les deux approches envisagées pour nommer les propriétés des objets ne sont pas complètement équivalentes. Les noms de propriété générés de façon aléatoire ou utilisant des espaces de noms ont un inconvénient: les clés correspondantes sont très faciles à trouver, surtout si le code recherche dans les clés des objets ou les sérialise. Prenons l'exemple suivant:

 const library2property = 'LIB2-NAMESPACE-id'; //    function lib2tag(obj) { obj[library2property] = 369; } const user = { name: 'Thomas Hunter II', age: 32 }; lib2tag(user); JSON.stringify(user); // '{"name":"Thomas Hunter II","age":32,"LIB2-NAMESPACE-id":369}' 

Si un symbole était utilisé pour le nom de clé dans cette situation, la représentation JSON de l'objet ne contiendrait pas la valeur du symbole. Pourquoi en est-il ainsi? Le fait est que le fait qu'un nouveau type de données soit apparu en JavaScript ne signifie pas que des modifications ont été apportées à la spécification JSON. JSON prend en charge, en tant que clés de propriété, uniquement les chaînes. Lors de la sérialisation d'un objet, aucune tentative n'est faite pour représenter les personnages d'une manière spéciale.

Le problème considéré d'obtenir des noms de propriété dans la représentation JSON des objets peut être résolu en utilisant Object.defineProperty() :

 const library2property = uuid(); //   function lib2tag(obj) { Object.defineProperty(obj, library2property, {   enumerable: false,   value: 369 }); } const user = { name: 'Thomas Hunter II', age: 32 }; lib2tag(user); // '{"name":"Thomas Hunter II",  "age":32,"f468c902-26ed-4b2e-81d6-5775ae7eec5d":369}' console.log(JSON.stringify(user)); console.log(user[library2property]); // 369 

Les clés de chaîne qui sont «cachées» en définissant leur descripteur enumerable sur false se comportent de la même manière que les clés représentées par des caractères. Les deux ne sont pas affichés lorsque Object.keys() appelé, et les deux peuvent être détectés à l'aide de Reflect.ownKeys() . Voici à quoi ça ressemble:

 const obj = {}; obj[Symbol()] = 1; Object.defineProperty(obj, 'foo', { enumberable: false, value: 2 }); console.log(Object.keys(obj)); // [] console.log(Reflect.ownKeys(obj)); // [ 'foo', Symbol() ] console.log(JSON.stringify(obj)); // {} 

Ici, je dois dire que nous avons presque recréé les possibilités des symboles, en utilisant d'autres moyens de JS. En particulier, les clés représentées par des symboles et les clés privées n'entrent pas dans la représentation JSON d'un objet. Les deux peuvent être reconnus en se référant à la méthode Reflect.ownKeys() . En conséquence, les deux ne peuvent pas être considérés comme vraiment privés. En supposant que certaines valeurs aléatoires ou espaces de noms de bibliothèque sont utilisés pour générer des noms de clés, cela signifie que nous nous sommes débarrassés du risque de collision de noms.

Cependant, il existe une petite différence entre l'utilisation de noms de symboles et les noms créés à l'aide d'autres mécanismes. Étant donné que les chaînes sont immuables et que les caractères sont garantis uniques, il existe toujours la possibilité que quelqu'un, après avoir parcouru toutes les combinaisons possibles de caractères dans une chaîne, provoque une collision de noms. D'un point de vue mathématique, cela signifie que les personnages nous donnent vraiment une opportunité précieuse que les chaînes n'ont pas.

Dans Node.js, lors de l'examen d'objets (par exemple, à l'aide de console.log() ), si une méthode d'objet appelée inspect détectée, cette méthode est utilisée pour obtenir une représentation sous forme de chaîne de l'objet, puis l'afficher à l'écran. Il est facile de comprendre qu'absolument tout le monde ne peut pas en tenir compte, donc un tel comportement du système peut conduire à un appel à la méthode d' inspect objet, qui est conçue pour résoudre des problèmes qui ne sont pas liés à la formation d'une représentation sous forme de chaîne de l'objet. Cette fonctionnalité est déconseillée dans Node.js 10, dans la version 11, les méthodes avec un nom similaire sont simplement ignorées. Maintenant, pour implémenter cette fonctionnalité, le require('util').inspect.custom . Cela signifie que personne ne pourra jamais perturber le système par inadvertance en créant une méthode objet appelée inspect .

Imitation de propriétés privées


Voici une approche intéressante que vous pouvez utiliser pour simuler les propriétés privées des objets. Cette approche implique l'utilisation d'une autre fonctionnalité JavaScript moderne: les objets proxy. Ces objets servent de wrappers pour d'autres objets qui permettent au programmeur d'intervenir dans les actions effectuées avec ces objets.

Les objets proxy offrent de nombreuses façons d'intercepter les actions effectuées sur les objets. Nous nous intéressons à la possibilité de contrôler le fonctionnement de la lecture des clés d'un objet. Nous n'entrerons pas dans les détails sur les objets proxy ici. Si vous êtes intéressé, consultez cette publication.

Nous pouvons utiliser des proxys pour contrôler quelles propriétés de l'objet sont visibles de l'extérieur. Dans ce cas, nous voulons créer un proxy qui cache deux propriétés que nous connaissons. L'un a le nom de chaîne _favColor et le second est représenté par un caractère écrit dans la variable favBook :

 let proxy; { const favBook = Symbol('fav book'); const obj = {   name: 'Thomas Hunter II',   age: 32,   _favColor: 'blue',   [favBook]: 'Metro 2033',   [Symbol('visible')]: 'foo' }; const handler = {   ownKeys: (target) => {     const reportedKeys = [];     const actualKeys = Reflect.ownKeys(target);     for (const key of actualKeys) {       if (key === favBook || key === '_favColor') {         continue;       }       reportedKeys.push(key);     }     return reportedKeys;   } }; proxy = new Proxy(obj, handler); } console.log(Object.keys(proxy)); // [ 'name', 'age' ] console.log(Reflect.ownKeys(proxy)); // [ 'name', 'age', Symbol(visible) ] console.log(Object.getOwnPropertyNames(proxy)); // [ 'name', 'age' ] console.log(Object.getOwnPropertySymbols(proxy)); // [Symbol(visible)] console.log(proxy._favColor); // 'blue 

Traiter une propriété dont le nom est représenté par la chaîne _favColor n'est pas difficile: il suffit de lire le code source. Les clés dynamiques (comme les clés uuid que nous avons vues ci-dessus) peuvent être associées à la force brute. Mais sans référence au symbole, vous ne pouvez pas accéder à la valeur de Metro 2033 partir de l'objet proxy .

Il convient de noter que dans Node.js, il existe une fonctionnalité qui viole la confidentialité des objets proxy. Cette fonctionnalité n'existe pas dans la langue elle-même, elle n'est donc pas pertinente pour d'autres runtimes JS, comme un navigateur. Le fait est que cette fonctionnalité vous permet d'accéder à l'objet caché derrière l'objet proxy, si vous avez accès à l'objet proxy. Voici un exemple qui montre la possibilité de contourner les mécanismes indiqués dans l'extrait de code précédent:

 const [originalObject] = process .binding('util') .getProxyDetails(proxy); const allKeys = Reflect.ownKeys(originalObject); console.log(allKeys[3]); // Symbol(fav book) 

Maintenant, pour empêcher l'utilisation de cette fonctionnalité dans une instance spécifique de Node.js, vous devez modifier l'objet Global Reflect ou la liaison du processus util . Cependant, c'est une autre tâche. Si vous êtes intéressé, consultez cet article sur la protection des API basées sur JavaScript.

Résumé


Dans cet article, nous avons parlé du type de données Symbol , des fonctionnalités qu'il fournit aux développeurs JavaScript et des mécanismes de langage existants qui peuvent être utilisés pour simuler ces fonctionnalités.

Chers lecteurs! Utilisez-vous des symboles dans vos projets JavaScript?

Source: https://habr.com/ru/post/fr444340/


All Articles