Un aperçu détaillé des symboles bien connus





Bonne journée, mes amis!



Symbol est un type de données primitif introduit dans ECMAScript2015 (ES6) qui vous permet de créer des identificateurs uniques: const uniqueKey = Symbol ('SymbolName').



Vous pouvez utiliser des symboles comme clés pour les propriétés des objets. Les symboles que JavaScript gère de manière spéciale sont appelés symboles connus . Ces caractères sont utilisés par les algorithmes JavaScript intégrés. Par exemple, Symbol.iterator est utilisé pour parcourir les éléments des tableaux, des chaînes. Il peut également être utilisé pour définir vos propres fonctions d'itérateur.



Ces symboles jouent un rôle important car ils vous permettent d'affiner le comportement des objets.



Étant unique, l'utilisation de symboles comme clés d'objet (au lieu de chaînes) facilite l'ajout de nouvelles fonctionnalités aux objets. En même temps, il n'y a pas lieu de s'inquiéter des collisions entre les clés (puisque chaque caractère est unique), ce qui peut devenir un problème lors de l'utilisation de chaînes.



Cet article se concentrera sur les symboles bien connus avec des exemples de leur utilisation.



Par souci de simplicité, la syntaxe des symboles bien connus Symbol. <nom> est au format @@ <nom>. Par exemple, Symbol.iterator est représenté par @@ iterator, Symbol.toPrimitive par @@ toPrimitive, etc.



Si nous disons qu'un objet a une méthode @@ iterator, alors l'objet contient une propriété appelée Symbol.iterator, représentée par une fonction: {[Symbol.iterator]: function () {}}.



1. Brève introduction aux symboles



Un caractère est un type primitif (tel qu'un nombre, une chaîne ou une valeur booléenne) qui est unique et immuable (immuable).



Pour créer un symbole, appelez la fonction Symbol () avec un argument optionnel - le nom ou, plus précisément, la description du symbole:



const mySymbol = Symbol()
const namedSymbol = Symbol('myName')
typeof mySymbol // symbol
typeof namedSymbol // symbol


mySymbol et namedSymbol sont des symboles primitifs. namedSymbol est nommé 'myName', qui est généralement utilisé pour le code de débogage.



Chaque appel à Symbol () crée un nouveau symbole unique. Deux caractères sont uniques (ou spéciaux) même s'ils portent le même nom:



const first = Symbol()
const second = Symbol()
first === second // false

const firstNamed = Symbol('Lorem')
const secondNamed = Symbol('Lorem')
firstNamed === secondNamed // false


Les symboles peuvent être des clés d'objets. Pour ce faire, utilisez la syntaxe de propriété calculée ([symbole]) dans un littéral d'objet ou une définition de classe:



const strSymbol = Symbol('String')

const myObj = {
  num: 1,
  [strSymbol]: 'Hello World'
}

myObj[strSymbol] // Hello World
Object.getOwnPropertyNames(myObj) // ['num']
Object.getOwnPropertySymbols(myObj) // [Symbol(String)]


Les propriétés de symbole ne peuvent pas être récupérées à l'aide d'Object.keys () ou d'Object.getOwnPropertyNames (). Pour y accéder, vous devez utiliser la fonction spéciale Object.getOwnPropertySymbols ().



L'utilisation de symboles connus comme clés vous permet de modifier le comportement des objets.



Les symboles connus sont disponibles en tant que propriétés non énumérables, immuables et non configurables de l'objet Symbol. Pour les obtenir, utilisez la notation par points: Symbol.iterator, Symbol.hasInstance, etc.



Voici comment obtenir une liste de symboles bien connus:



Object.getOwnPropertyNames(Symbol)
// ["hasInstance", "isConcatSpreadable", "iterator", "toPrimitive",
//  "toStringTag", "unscopables", "match", "replace", "search",
//  "split", "species", ...]

typeof Symbol.iterator // symbol


Object.getOwnPropertyNames (Symbol) renvoie une liste des propriétés natives de l'objet Symbol, y compris les symboles connus. Symbol.iterator est bien sûr de type symbole.



2. @@ iterator, qui vous permet de rendre les objets itérables (itérables)



Symbol.iterator est peut-être le symbole le plus connu. Il vous permet de définir comment un objet doit être itéré en utilisant une instruction for-of ou un opérateur de propagation (et s'il doit être itéré du tout).



De nombreux types intégrés tels que les chaînes, les tableaux, les cartes, les ensembles ou les ensembles sont itérables par défaut car ils ont une méthode @@ iterator:



const myStr = 'Hi'
typeof myStr[Symbol.iterator] // function
for (const char of myStr) {
  console.log(char) //     :  'H',  'i'
}
[...myStr] // ['H', 'i']


La variable myStr contient une chaîne primitive qui a une propriété Symbol.iterator. Cette propriété contient une fonction utilisée pour parcourir les caractères d'une chaîne.



L'objet dans lequel la méthode Symbol.iterator est définie doit être conforme au protocole d' itération (itérateur) . Plus précisément, cette méthode doit renvoyer un objet conforme au protocole spécifié. Un tel objet doit avoir une méthode next () qui renvoie {value: <iterator_value>, done: <boolean_finished_iterator>}.



Dans l'exemple suivant, nous créons un objet myMethods itérable pour itérer sur ses méthodes:



function methodsIterator() {
  let index = 0
  const methods = Object.keys(this)
    .filter(key => typeof this[key] === 'function')

    return {
      next: () => ({
        done: index === methods.length,
        value: methods[index++]
      })
    }
}

const myMethods = {
  toString: () => '[object myMethods]',
  sum: (a, b) => a + b,
  numbers: [1, 3, 5],
  [Symbol.iterator]: methodsIterator
}

for (const method of myMethods) {
  console.log(method) // toString, sum
}


methodsIterator () est une fonction qui renvoie un itérateur {next: function () {}}. L'objet myMethods définit une propriété calculée [Symbol.iterator] avec la valeur methodsIterator. Cela rend l'objet itérable à l'aide d'une boucle for-of. Les méthodes objet peuvent également être obtenues en utilisant [... myMethods]. Un tel objet peut être converti en tableau en utilisant Array.from (myMethods).



La création d'un itérable peut être simplifiée en utilisant une fonction générateur . Cette fonction renvoie un objet Generator conforme au protocole d'itération.



Créons une classe Fibonacci avec une méthode @@ iterator qui génère une séquence de nombres de Fibonacci:



class Fibonacci {
  constructor(n) {
    this.n = n
  }

  *[Symbol.iterator]() {
    let a = 0, b = 1, index = 0
    while (index < this.n) {
      index++
      let current = a
      a = b
      b = current + a
      yield current
    }
  }
}

const sequence = new Fibonacci(6)
const numbers = [...sequence]
console.log(numbers) // [0, 1, 1, 2, 3, 5]


* [Symbol.iterator] () {} définit une méthode de classe - une fonction génératrice. L'instance de Fibonacci est conforme au protocole de force brute. L'opérateur de diffusion appelle la méthode @@ iterator pour créer un tableau de nombres.



Si un type ou un objet primitif contient @@ iterator, il peut être utilisé dans les scénarios suivants:



  • Boucle à travers des éléments avec for-of
  • Création d'un tableau d'éléments à l'aide de l'opérateur d'étalement
  • Création d'un tableau à l'aide de Array.from (iterableObject)
  • Dans une expression yield * à passer à un autre générateur
  • Dans les constructeurs Map (), WeakMap (), Set () et WeakSet ()
  • Dans les méthodes statiques Promise.all (), Promise.race (), etc.


Vous pouvez en savoir plus sur la création d'un objet itérable ici .



3. @@ hasInstance pour configurer instanceof



Par défaut, l'opérateur obj instanceof Constructor vérifie s'il existe un objet Constructor.prototype dans la chaîne de prototypes obj. Prenons un exemple:



function Constructor() {
  // ...
}
const obj = new Constructor()
const objProto = Object.getPrototypeOf(obj)

objProto === Constructor.prototype // true
obj instanceof Constructor // true
obj instanceof Object // true


obj instanceof Constructor renvoie true car le prototype d'obj est Constructor.prototype (suite à l'appel du constructeur). instanceof fait référence à la chaîne de prototypes si nécessaire, donc obj instanceof Object renvoie également true.



Parfois, une application nécessite une vérification d'instance plus stricte.



Heureusement, nous avons la possibilité de définir une méthode @@ hasInstance pour changer le comportement de instanceof. obj instanceof Type est équivalent à Type [Symbol.hasInstance] (obj).



Vérifions si les variables sont itérables:



class Iterable {
  static [Symbol.hasInstance](obj) {
    return typeof obj[Symbol.iterator] === 'function'
  }
}

const arr = [1, 3, 5]
const str = 'Hi'
const num = 21
arr instanceof Iterable // true
str instanceof Iterable // true
num instanceof Iterable // false


La classe Iterable contient une méthode statique @@ hasInstance. Cette méthode vérifie si obj est itérable, i.e. s'il contient une propriété Symbol.iterator. arr et str sont itérables, mais num ne l'est pas.



4. @@ toPrimitive pour convertir un objet en primitive



Utilisez Symbol.toPrimitive pour définir une propriété dont la valeur est une fonction de conversion d'objet en primitive. @@ toPrimitive prend un paramètre, indice, qui peut être un nombre, une chaîne ou une valeur par défaut. hint indique le type de la valeur de retour.



Améliorons la transformation du tableau:



function arrayToPrimitive(hint) {
  if (hint === 'number') {
    return this.reduce((x, y) => x + y)
  } else if (hint === 'string') {
    return `[${this.join(', ')}]`
  } else {
    // hint    
    return this.toString()
  }
}

const array = [1, 3, 5]
array[Symbol.toPrimitive] = arrayToPrimitive

//    . hint  
+ array // 9
//    . hint  
`array is ${array}` // array is [1, 3, 5]
//   . hint   default
'array elements: ' + array // array elements: 1,3,5


arrayToPrimitive (hint) est une fonction qui convertit un tableau en primitive en fonction de la valeur de l'indice. La définition de array [Symbol.toPrimitive] sur arrayToPrimitive force le tableau à utiliser la nouvelle méthode de transformation. Faire + tableau appelle @@ toPrimitive avec une valeur d'indication de nombre. La somme des éléments du tableau est renvoyée. array est $ {array} appelle @@ toPrimitive avec hint = string. Le tableau est converti en la chaîne «[1, 3, 5]». Enfin 'array elements:' + array utilise hint = default pour transformer. Le tableau est converti en «1,3,5».



La méthode @@ toPrimitive est utilisée pour représenter un objet en tant que type primitif:



  • Lors de l'utilisation de l'opérateur d'égalité lâche (abstrait): object == primitive
  • Lors de l'utilisation de l'opérateur d'addition / concaténation: objet + primitive
  • Lors de l'utilisation de l'opérateur de soustraction: objet - primitif
  • Dans diverses situations, convertir un objet en primitive: chaîne (objet), nombre (objet), etc.


5. @@ toStringTag pour créer une description d'objet standard



Utilisez Symbol.toStringTag pour définir une propriété dont la valeur est une chaîne qui décrit le type de l'objet. La méthode @@ toStringTag est utilisée par Object.prototype.toString ().



La spécification définit les valeurs par défaut renvoyées par Object.prototype.toString () pour de nombreux types:



const toString = Object.prototype.toString
toString.call(undefined) // [object Undefined]
toString.call(null)      // [object Null]
toString.call([1, 4])    // [object Array]
toString.call('Hello')   // [object String]
toString.call(15)        // [object Number]
toString.call(true)      // [object Boolean]
// Function, Arguments, Error, Date, RegExp  ..
toString.call({})        // [object Object]


Ces types n'ont pas de propriété Symbol.toStringTag car l'algorithme Object.prototype.toString () les évalue d'une manière spéciale.



La propriété en question est définie dans des types tels que des symboles, des fonctions de générateur, des cartes, des promesses, etc. Prenons un exemple:



const toString = Object.prototype.toString
const noop = function() { }

Symbol.iterator[Symbol.toStringTag]   // Symbol
(function* () {})[Symbol.toStringTag] // GeneratorFunction
new Map()[Symbol.toStringTag]         // Map
new Promise(noop)[Symbol.toStringTag] // Promise

toString.call(Symbol.iterator)   // [object Symbol]
toString.call(function* () {})   // [object GeneratorFunction]
toString.call(new Map())         // [object Map]
toString.call(new Promise(noop)) // [object Promise]


Dans le cas où l'objet n'est pas d'un groupe de type standard et ne contient pas la propriété @@ toStringTag, Object est retourné. Bien sûr, nous pouvons changer cela:



const toString = Object.prototype.toString

class SimpleClass { }
toString.call(new SimpleClass) // [object Object]

class MyTypeClass {
  constructor() {
    this[Symbol.toStringTag] = 'MyType'
  }
}

toString.call(new MyTypeClass) // [object MyType]


Une instance de la classe SimpleClass n'a pas de propriété @@ toStringTag, donc Object.prototype.toString () renvoie [object Object]. Le constructeur de la classe MyTypeClass affecte la propriété @@ toStringTag à l'instance avec la valeur MyType, donc Object.prototype.toString () renvoie [object MyType].



Notez que @@ toStringTag a été introduit pour la compatibilité descendante. Son utilisation est indésirable. Il est préférable d'utiliser instanceof (avec @@ hasInstance) ou typeof pour déterminer le type d'un objet.



6. @@ species pour créer un objet dérivé



Utilisez Symbol.species pour définir une propriété dont la valeur est une fonction constructeur utilisée pour créer des objets dérivés.



La valeur @@ species de nombreux constructeurs est celle des constructeurs eux-mêmes:



Array[Symbol.species] === Array // true
Map[Symbol.species] === Map // true
RegExp[Symbol.species] === RegExp // true


Tout d'abord, notez qu'un objet dérivé est un objet qui est renvoyé après avoir effectué une opération spécifique sur l'objet d'origine. Par exemple, l'appel de map () renvoie un objet dérivé - le résultat de la transformation des éléments du tableau.



En général, les objets dérivés font référence au même constructeur que les objets d'origine. Mais parfois, il devient nécessaire de définir un autre constructeur (peut-être l'une des classes standard): c'est là que @@ species peut aider.



Supposons que nous étendions le constructeur Array avec la classe enfant MyArray pour ajouter des méthodes utiles. Ce faisant, nous voulons que le constructeur des objets dérivés de l'instance MyArray soit Array. Pour ce faire, vous devez définir une propriété calculée @@ species avec une valeur Array:



class MyArray extends Array {
  isEmpty() {
    return this.length === 0
  }
  static get [Symbol.species]() {
    return Array
  }
}
const array = new MyArray(2, 3, 5)
array.isEmpty() // false
const odds = array.filter(item => item % 2 === 1)
odds instanceof Array // true
odds instanceof MyArray // false


MyArray définit la propriété calculée statique Symbol.species. Il spécifie que le constructeur des objets dérivés doit être le constructeur Array. Plus tard, lors du filtrage des éléments du tableau, array.filter () renvoie Array.



La propriété calculée @@ species est utilisée par les méthodes de tableau et de tableau typé telles que map (), concat (), slice (), splice (), qui renvoient des objets dérivés. L'utilisation de cette propriété peut être utile pour étendre des cartes, des expressions régulières ou des promesses tout en préservant le constructeur d'origine.



7. Créez une expression régulière sous la forme d'un objet: @@ match, @@ replace, @@ search et @@ split



Le prototype de chaîne contient 4 méthodes qui prennent des expressions régulières comme argument:



  • String.prototype.match (regExp)
  • String.prototype.replace (regExp, newSubstr)
  • String.prototype.search (regExp)
  • String.prototype.split (regExp, limite)


ES6 permet à ces méthodes d'accepter d'autres types en définissant les propriétés calculées correspondantes: @@ match, @@ replace, @@ search et @@ split.



Curieusement, le prototype RegExp contient les méthodes spécifiées, également définies à l'aide de symboles:



typeof RegExp.prototype[Symbol.match]   // function
typeof RegExp.prototype[Symbol.replace] // function
typeof RegExp.prototype[Symbol.search]  // function
typeof RegExp.prototype[Symbol.split]   // function


Dans l'exemple suivant, nous définissons une classe qui peut être utilisée à la place d'une expression régulière:



class Expression {
  constructor(pattern) {
    this.pattern = pattern
  }
  [Symbol.match](str) {
    return str.includes(this.pattern)
  }
  [Symbol.replace](str, replace) {
    return str.split(this.pattern).join(replace)
  }
  [Symbol.search](str) {
    return str.indexOf(this.pattern)
  }
  [Symbol.split](str) {
    return str.split(this.pattern)
  }
}

const sunExp = new Expression('')

' '.match(sunExp) // true
' '.match(sunExp) // false
' day'.replace(sunExp, '') // ' '
'  '.search(sunExp) // 8
''.split(sunExp) // ['', '']


La classe Expression définit les méthodes @@ match, @@ replace, @@ search et @@ split. Ensuite, une instance de cette classe - sunExp est utilisée dans les méthodes appropriées au lieu d'une expression régulière.



8. @@ isConcatSpreadable pour convertir l'objet en tableau



Symbol.isConcatSpreadable est une valeur booléenne indiquant qu'un objet peut être converti en tableau à l'aide de la méthode Array.prototype.concat ().



Par défaut, la méthode concat () récupère les éléments du tableau (décompose le tableau en les éléments dont il se compose) lors de la concaténation des tableaux:



const letters = ['a', 'b']
const otherLetters = ['c', 'd']
otherLetters.concat('e', letters) // ['c', 'd', 'e', 'a', 'b']


Pour concaténer les deux tableaux, passez des lettres comme argument à la méthode concat (). Les éléments du tableau de lettres font partie du résultat de la concaténation: ['c', 'd', 'e', ​​'a', 'b'].



Pour éviter que le tableau ne soit décomposé en éléments et pour que le tableau fasse partie du résultat de l'union tel quel, la propriété @@ isConcatSpreadable doit être définie sur false:



const letters = ['a', 'b']
letters[Symbol.isConcatSpreadable] = false
const otherLetters = ['c', 'd']
otherLetters.concat('e', letters) // ['c', 'd', 'e', ['a', 'b']]


Contrairement à un tableau, la méthode concat () ne décompose pas les objets de type tableau en éléments. Ce comportement peut également être modifié avec @@ isConcatSpreadable:



const letters = { 0: 'a', 1: 'b', length: 2 }
const otherLetters = ['c', 'd']
otherLetters.concat('e', letters)
// ['c', 'd', 'e', {0: 'a', 1: 'b', length: 2}]
letters[Symbol.isConcatSpreadable] = true
otherLetters.concat('e', letters) // ['c', 'd', 'e', 'a', 'b']


9. @@ unscopables pour accéder aux propriétés avec avec



Symbol.unscopables est une propriété calculée dont les noms propres sont exclus de l'objet ajouté au début de la chaîne de portée à l'aide de l'instruction with. La propriété @@ unscopables a le format suivant: {propertyName: <boolean_exclude_binding>}.



ES6 définit @@ unscopables pour les tableaux uniquement. Ceci est fait afin de masquer les nouvelles méthodes qui peuvent écraser les variables du même nom dans l'ancien code:



Array.prototype[Symbol.unscopables]
// { copyWithin: true, entries: true, fill: true,
//   find: true, findIndex: true, keys: true }
let numbers = [1, 3, 5]
with (numbers) {
  concat(7) // [1, 3, 5, 7]
  entries // ReferenceError: entries is not defined
}


Nous pouvons accéder à la méthode concat () dans le corps with, car cette méthode n'est pas contenue dans la propriété @@ unscopables. La méthode entries () est spécifiée dans cette propriété et est définie sur true, ce qui la rend indisponible à l'intérieur avec.



@@ unscopables a été introduit uniquement pour la compatibilité descendante avec le code hérité en utilisant l'instruction with (obsolète et interdite en mode strict).



J'espère que vous avez trouvé quelque chose d'intéressant pour vous-même. Merci de votre attention.



All Articles