Una descripción detallada de los símbolos conocidos





¡Buen dia amigos!



Símbolo es un tipo de datos primitivo introducido en ECMAScript2015 (ES6) que le permite crear identificadores únicos: const uniqueKey = Symbol ('SymbolName').



Puede utilizar símbolos como claves para las propiedades del objeto. Los símbolos que JavaScript maneja de forma especial se denominan Símbolos conocidos . Estos caracteres son utilizados por algoritmos JavaScript integrados. Por ejemplo, Symbol.iterator se usa para iterar sobre los elementos de matrices, cadenas. También se puede utilizar para definir sus propias funciones de iterador.



Estos símbolos juegan un papel importante, ya que le permiten ajustar el comportamiento de los objetos.



Al ser único, el uso de símbolos como claves de objetos (en lugar de cadenas) facilita la adición de nuevas funciones a los objetos. Al mismo tiempo, no hay necesidad de preocuparse por las colisiones entre claves (ya que cada carácter es único), lo que puede convertirse en un problema al usar cadenas.



Este artículo se centrará en símbolos conocidos con ejemplos de su uso.



En aras de la simplicidad, la sintaxis de los símbolos conocidos de Symbol. <nombre> está en el formato @@ <nombre>. Por ejemplo, Symbol.iterator se representa como @@ iterator, Symbol.toPrimitive como @@ toPrimitive, etc.



Si decimos que un objeto tiene un método @@ iterator, entonces el objeto contiene una propiedad llamada Symbol.iterator, representada por una función: {[Symbol.iterator]: function () {}}.



1. Breve introducción a los símbolos



Un carácter es un tipo primitivo (como un número, una cadena o un booleano), único e inmutable (inmutable).



Para crear un símbolo, llame a la función Symbol () con un argumento opcional: el nombre o, más precisamente, la descripción del símbolo:



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


mySymbol y namedSymbol son símbolos primitivos. namedSymbol se llama 'myName', que generalmente se usa para depurar código.



Cada llamada a Symbol () crea un nuevo símbolo único. Dos caracteres son únicos (o especiales) incluso si tienen el mismo nombre:



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

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


Los símbolos pueden ser claves de objetos. Para hacer esto, use la sintaxis de propiedad calculada ([símbolo]) en una definición de clase o literal de objeto:



const strSymbol = Symbol('String')

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

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


Las propiedades de los símbolos no se pueden recuperar mediante Object.keys () u Object.getOwnPropertyNames (). Para acceder a ellos, debe utilizar la función especial Object.getOwnPropertySymbols ().



El uso de símbolos conocidos como teclas le permite cambiar el comportamiento de los objetos.



Los símbolos conocidos están disponibles como propiedades no enumerables, inmutables y no configurables del objeto Símbolo. Para obtenerlos, use la notación de puntos: Symbol.iterator, Symbol.hasInstance, etc.



A continuación, le indicamos cómo obtener una lista de símbolos conocidos:



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

typeof Symbol.iterator // symbol


Object.getOwnPropertyNames (Symbol) devuelve una lista de las propiedades nativas del objeto Symbol, incluidos los símbolos conocidos. Symbol.iterator es de tipo símbolo, por supuesto.



2. @@ iterador, que le permite hacer que los objetos sean iterables (iterables)



Symbol.iterator es quizás el símbolo más conocido. Le permite definir cómo se debe iterar un objeto utilizando una instrucción for-of o un operador de extensión (y si se debe iterar en absoluto).



Muchos tipos integrados, como cadenas, matrices, mapas, conjuntos o conjuntos son iterables de forma predeterminada porque tienen un método @@ iterador:



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


La variable myStr contiene una cadena primitiva que tiene una propiedad Symbol.iterator. Esta propiedad contiene una función que se utiliza para iterar sobre los caracteres de una cadena.



El objeto en el que se define el método Symbol.iterator debe ajustarse al protocolo de iteración (iterador) . Más precisamente, este método debe devolver un objeto que se ajuste al protocolo especificado. Dicho objeto debe tener un método next () que devuelva {value: <iterator_value>, done: <boolean_finished_iterator>}.



En el siguiente ejemplo, creamos un objeto myMethods iterable para iterar sobre sus métodos:



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 () es una función que devuelve un iterador {siguiente: función () {}}. El objeto myMethods define una propiedad calculada [Symbol.iterator] con el valor methodsIterator. Esto hace que el objeto sea iterable usando un bucle for-of. Los métodos de objeto también se pueden obtener usando [... myMethods]. Dicho objeto se puede convertir en una matriz usando Array.from (myMethods).



La creación de un iterable se puede simplificar utilizando una función generadora . Esta función devuelve un objeto Generator que se ajusta al protocolo de iteración.



Creemos una clase de Fibonacci con un método de iterador @@ que genera una secuencia de números 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] () {} define un método de clase: una función generadora. La instancia de Fibonacci sigue el protocolo de fuerza bruta. El operador de propagación llama al método @@ iterador para crear una matriz de números.



Si un tipo u objeto primitivo contiene @@ iterador, se puede utilizar en los siguientes escenarios:



  • Iterando elementos con for-of
  • Creando una matriz de elementos usando el operador de propagación
  • Creando una matriz usando Array.from (iterableObject)
  • En una expresión de rendimiento * para pasar a otro generador
  • En los constructores Map (), WeakMap (), Set () y WeakSet ()
  • En métodos estáticos Promise.all (), Promise.race (), etc.


Puede leer más sobre la creación de un objeto iterable aquí .



3. @@ hasInstance para configurar la instancia de



Por defecto, la instancia obj del operador Constructor comprueba si la cadena del prototipo obj contiene un objeto Constructor.prototype. Consideremos un ejemplo:



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 devuelve verdadero porque el prototipo de obj es Constructor.prototype (como resultado de llamar al constructor). instanceof se refiere a la cadena de prototipos según sea necesario, por lo que obj instanceof Object también devuelve verdadero.



A veces, una aplicación necesita una verificación de instancia más estricta.



Afortunadamente, tenemos la capacidad de definir un método @@ hasInstance para cambiar el comportamiento de instanceof. obj instanceof Type es equivalente a Type [Symbol.hasInstance] (obj).



Comprobemos si las variables son iterables:



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 clase Iterable contiene un método estático @@ hasInstance. Este método comprueba si obj es iterable, es decir si contiene una propiedad Symbol.iterator. arr y str son iterables, pero num no lo es.



4. @@ toPrimitive para convertir un objeto en primitivo



Utilice Symbol.toPrimitive para definir una propiedad cuyo valor es un objeto a función de conversión primitiva. @@ toPrimitive toma un parámetro, sugerencia, que puede ser número, cadena o predeterminado. pista indica el tipo de valor de retorno.



Mejoremos la transformación de la matriz:



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 (sugerencia) es una función que convierte una matriz en una primitiva según el valor de la sugerencia. Establecer matriz [Symbol.toPrimitive] en arrayToPrimitive fuerza a la matriz a utilizar el nuevo método de transformación. Haciendo + matriz llama a @@ toPrimitive con un valor de sugerencia de número. Se devuelve la suma de los elementos de la matriz. array es $ {array} llama a @@ toPrimitive con hint = string. La matriz se convierte en la cadena '[1, 3, 5]'. Finalmente, 'elementos de matriz:' + matriz usa hint = default para transformar. La matriz se convierte en '1,3,5'.



El método @@ toPrimitive se usa para representar un objeto como un tipo primitivo:



  • Cuando se usa el operador de igualdad suelto (abstracto): objeto == primitivo
  • Cuando se usa el operador de suma / concatenación: objeto + primitiva
  • Cuando se usa el operador de resta: objeto - primitiva
  • En varias situaciones, convertir un objeto en una primitiva: Cadena (objeto), Número (objeto), etc.


5. @@ toStringTag para crear una descripción de objeto estándar



Utilice Symbol.toStringTag para definir una propiedad cuyo valor es una cadena que describe el tipo de objeto. Object.prototype.toString () utiliza el método @@ toStringTag.



La especificación define los valores predeterminados devueltos por Object.prototype.toString () para muchos tipos:



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]


Estos tipos no tienen una propiedad Symbol.toStringTag porque el algoritmo Object.prototype.toString () los evalúa de una manera especial.



La propiedad en cuestión se define en tipos como símbolos, funciones generadoras, tarjetas, promesas, etc. Considere un ejemplo:



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]


En el caso de que el objeto no sea de un grupo de tipos estándar y no contenga la propiedad @@ toStringTag, se devuelve Object. Por supuesto, podemos cambiar esto:



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]


Una instancia de la clase SimpleClass no tiene una propiedad @@ toStringTag, por lo que Object.prototype.toString () devuelve [object Object]. El constructor de la clase MyTypeClass asigna la propiedad @@ toStringTag a la instancia con el valor MyType, por lo que Object.prototype.toString () devuelve [object MyType].



Tenga en cuenta que @@ toStringTag se introdujo por motivos de compatibilidad con versiones anteriores. Su uso es indeseable. Es mejor usar instanceof (junto con @@ hasInstance) o typeof para determinar el tipo de un objeto.



6. @@ especies para crear un objeto derivado



Utilice Symbol.species para definir una propiedad cuyo valor es una función constructora que se utiliza para crear objetos derivados.



El valor de la especie @@ de muchos constructores son los propios constructores:



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


Primero, observe que un objeto derivado es un objeto que se devuelve después de realizar una operación específica en el objeto original. Por ejemplo, llamar a map () devuelve un objeto derivado, el resultado de transformar los elementos de la matriz.



Por lo general, los objetos derivados se refieren al mismo constructor que los objetos originales. Pero a veces se hace necesario definir otro constructor (tal vez una de las clases estándar): aquí es donde las especies @@ pueden ayudar.



Supongamos que ampliamos el constructor Array con la clase secundaria MyArray para agregar algunos métodos útiles. Al hacerlo, queremos que el constructor de los objetos derivados de la instancia de MyArray sea Array. Para hacer esto, necesita definir una propiedad calculada @@ especie con un valor 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 define la propiedad calculada estática Symbol.species. Especifica que el constructor de objetos derivados debe ser el constructor Array. Más adelante, cuando se filtran los elementos de una matriz, array.filter () devuelve una matriz.



La propiedad calculada @@ species es utilizada por métodos de matriz y matriz con tipo como map (), concat (), slice (), splice (), que devuelven objetos derivados. El uso de esta propiedad puede resultar útil para ampliar mapas, expresiones regulares o promesas conservando el constructor original.



7. Cree una expresión regular en forma de objeto: @@ coincidir, @@ reemplazar, @@ buscar y @@ dividir



El prototipo de cadena contiene 4 métodos que toman expresiones regulares como argumento:



  • String.prototype.match (regExp)
  • String.prototype.replace (regExp, newSubstr)
  • String.prototype.search (regExp)
  • String.prototype.split (regExp, límite)


ES6 permite que estos métodos acepten otros tipos definiendo las propiedades calculadas correspondientes: @@ coincidencia, @@ reemplazar, @@ búsqueda y @@ dividir.



Curiosamente, el prototipo de RegExp contiene los métodos especificados, también definidos mediante símbolos:



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


En el siguiente ejemplo, estamos definiendo una clase que se puede usar en lugar de una expresión regular:



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 clase Expression define los métodos @@ match, @@ replace, @@ search y @@ split. Luego, se usa una instancia de esta clase, sunExp, en los métodos apropiados en lugar de una expresión regular.



8. @@ isConcatSpreadable para convertir un objeto en una matriz



Symbol.isConcatSpreadable es un valor booleano que indica que el objeto se puede convertir en una matriz utilizando el método Array.prototype.concat ().



Por defecto, el método concat () recupera los elementos de la matriz (descompone la matriz en los elementos que la componen) al concatenar las matrices:



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


Para concatenar las dos matrices, pase letras como argumento al método concat (). Los elementos de la matriz de letras pasan a formar parte del resultado de la concatenación: ['c', 'd', 'e', ​​'a', 'b'].



Para evitar que la matriz se descomponga en elementos y hacer que la matriz forme parte del resultado de la unión tal como está, la propiedad @@ isConcatSpreadable debe establecerse en false:



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


A diferencia de una matriz, el método concat () no descompone objetos similares a una matriz en elementos. Este comportamiento también se puede cambiar con @@ 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 para acceder a las propiedades con



Symbol.unscopables es una propiedad calculada cuyos nombres propios se excluyen del objeto agregado al principio de la cadena de alcance mediante la instrucción with. La propiedad @@ unscopables tiene el siguiente formato: {propertyName: <boolean_exclude_binding>}.



ES6 define @@ nocopables solo para matrices. Esto se hace para ocultar nuevos métodos que pueden sobrescribir variables del mismo nombre en el código anterior:



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
}


Podemos acceder al método concat () en el cuerpo with, ya que este método no está contenido en la propiedad @@ unscopables. El método entries () se especifica en esta propiedad y se establece en true, lo que hace que no esté disponible dentro de.



@@ unscopables se introdujo únicamente por compatibilidad con versiones anteriores del código heredado mediante la declaración with (obsoleta y no permitida en modo estricto).



Espero que hayas encontrado algo interesante para ti. Gracias por su atención.



All Articles