Uma visão geral detalhada de símbolos conhecidos





Bom dia amigos!



Símbolo é um tipo de dado primitivo introduzido em ECMAScript2015 (ES6) que permite criar identificadores únicos: const uniqueKey = Symbol ('SymbolName').



Você pode usar símbolos como chaves para propriedades de objetos. Os símbolos que o JavaScript trata de uma maneira especial são chamados de símbolos conhecidos . Esses caracteres são usados ​​por algoritmos JavaScript integrados. Por exemplo, Symbol.iterator é usado para iterar sobre os elementos de arrays, strings. Também pode ser usado para definir suas próprias funções de iterador.



Esses símbolos desempenham um papel importante, pois permitem ajustar o comportamento dos objetos.



Sendo único, o uso de símbolos como chaves de objeto (em vez de strings) facilita a adição de novas funcionalidades aos objetos. Ao mesmo tempo, não há necessidade de se preocupar com colisões entre chaves (já que cada caractere é único), o que pode se tornar um problema ao usar strings.



Este artigo se concentrará em símbolos conhecidos com exemplos de seu uso.



Por uma questão de simplicidade, a sintaxe do conhecido Symbol. <nome> símbolos está no formato @@ <nome>. Por exemplo, Symbol.iterator é representado como @@ iterator, Symbol.toPrimitive como @@ toPrimitive, etc.



Se dissermos que um objeto tem um método @@ iterator, então o objeto contém uma propriedade chamada Symbol.iterator, representada por uma função: {[Symbol.iterator]: function () {}}.



1. Breve introdução aos símbolos



Um caractere é um tipo primitivo (como um número, string ou booleano), único e imutável (imutável).



Para criar um símbolo, chame a função Symbol () com um argumento opcional - o nome ou, mais precisamente, a descrição do símbolo:



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


mySymbol e namedSymbol são símbolos primitivos. namedSymbol é denominado 'myName', que geralmente é usado para depurar código.



Cada chamada para Symbol () cria um novo símbolo exclusivo. Dois caracteres são únicos (ou especiais), mesmo que tenham o mesmo nome:



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

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


Os símbolos podem ser chaves de objetos. Para fazer isso, você deve usar a sintaxe de propriedade computada ([símbolo]) em um objeto literal ou definição 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)]


As propriedades do símbolo não podem ser recuperadas usando Object.keys () ou Object.getOwnPropertyNames (). Para acessá-los, você precisa usar a função especial Object.getOwnPropertySymbols ().



O uso de símbolos conhecidos como chaves permite alterar o comportamento dos objetos.



Símbolos conhecidos estão disponíveis como propriedades não enumeráveis, imutáveis ​​e não configuráveis ​​do objeto Symbol. Para obtê-los, use a notação de ponto: Symbol.iterator, Symbol.hasInstance, etc.



Veja como obter uma lista de símbolos conhecidos:



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

typeof Symbol.iterator // symbol


Object.getOwnPropertyNames (Symbol) retorna uma lista das propriedades nativas do objeto Symbol, incluindo símbolos bem conhecidos. Symbol.iterator é do tipo símbolo, é claro.



2. @@ iterator, que permite que você torne objetos iteráveis ​​(iteráveis)



Symbol.iterator é talvez o símbolo mais conhecido. Ele permite que você defina como um objeto deve ser iterado usando uma instrução for-of ou um operador de propagação (e se ele deve ser iterado).



Muitos tipos integrados, como strings, arrays, mapas, conjuntos ou conjuntos, são iteráveis ​​por padrão porque têm um método @@ iterador:



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


A variável myStr contém uma string primitiva que possui uma propriedade Symbol.iterator. Esta propriedade contém uma função usada para iterar os caracteres em uma string.



O objeto no qual o método Symbol.iterator é definido deve estar em conformidade com o protocolo de iteração (iterador) . Mais precisamente, esse método deve retornar um objeto que esteja em conformidade com o protocolo especificado. Tal objeto deve ter um método next () que retorna {valor: <iterator_value>, feito: <boolean_finished_iterator>}.



No exemplo a seguir, criamos um objeto myMethods iterável para iterar seus 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 () é uma função que retorna um iterador {next: function () {}}. O objeto myMethods define uma propriedade computada [Symbol.iterator] com o valor methodsIterator. Isso torna o objeto iterável usando um loop for-of. Os métodos de objeto também podem ser obtidos usando [... myMethods]. Esse objeto pode ser convertido em uma matriz usando Array.from (myMethods).



A criação de um objeto iterável pode ser simplificada usando uma função geradora . Esta função retorna um objeto Generator que está em conformidade com o protocolo de iteração.



Vamos criar uma classe de Fibonacci com um método @@ iterator que gera uma sequência 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 um método de classe - uma função geradora. A instância Fibonacci está em conformidade com o protocolo de força bruta. O operador spread chama o método @@ iterator para criar uma matriz de números.



Se um tipo ou objeto primitivo contém @@ iterator, ele pode ser usado nos seguintes cenários:



  • Loop através de elementos com for-of
  • Criação de um array de elementos usando o operador spread
  • Criação de uma matriz usando Array.from (iterableObject)
  • Em uma expressão de rendimento * para passar para outro gerador
  • Nos construtores Map (), WeakMap (), Set () e WeakSet ()
  • Nos métodos estáticos Promise.all (), Promise.race (), etc.


Você pode ler mais sobre como criar um objeto iterável aqui .



3. @@ hasInstance para configurar instanceof



Por padrão, o operador obj instanceof Constructor verifica se há um objeto Constructor.prototype na cadeia de protótipo obj. Vamos considerar um exemplo:



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 retorna true porque o protótipo de obj é Constructor.prototype (como resultado da chamada do construtor). instanceof se refere à cadeia de protótipo conforme necessário, então obj instanceof Object também retorna true.



Às vezes, um aplicativo precisa de uma verificação de instância mais rigorosa.



Felizmente, temos a capacidade de definir um método @@ hasInstance para alterar o comportamento de instanceof. obj instanceof Type é equivalente a Type [Symbol.hasInstance] (obj).



Vamos verificar se as variáveis ​​são iteráveis:



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


A classe Iterable contém um método estático @@ hasInstance. Este método verifica se obj é iterável, ou seja, se contém uma propriedade Symbol.iterator. arr e str são iteráveis, mas num não.



4. @@ toPrimitive para converter um objeto em um primitivo



Use Symbol.toPrimitive para definir uma propriedade cujo valor é um objeto para a função de conversão primitiva. @@ toPrimitive aceita um parâmetro, dica, que pode ser número, string ou padrão. dica indica o tipo do valor de retorno.



Vamos melhorar a transformação do array:



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) é uma função que converte uma matriz em uma primitiva com base no valor da dica. Definir array [Symbol.toPrimitive] como arrayToPrimitive força o array a usar o novo método de transformação. Fazer + array chama @@ toPrimitive com um valor de dica de número. A soma dos elementos da matriz é retornada. array é $ {array} chama @@ toPrimitive com hint = string. A matriz é convertida na string '[1, 3, 5]'. Finalmente 'array elements:' + array usa hint = default para transformar. A matriz é convertida em '1,3,5'.



O método @@ toPrimitive é usado para representar um objeto como um tipo primitivo:



  • Ao usar o operador de igualdade flexível (abstrato): objeto == primitivo
  • Ao usar o operador de adição / concatenação: objeto + primitivo
  • Ao usar o operador de subtração: objeto - primitivo
  • Em várias situações, convertendo um objeto em um primitivo: String (objeto), Número (objeto), etc.


5. @@ toStringTag para criar uma descrição de objeto padrão



Use Symbol.toStringTag para definir uma propriedade cujo valor é uma string que descreve o tipo do objeto. O método @@ toStringTag é usado por Object.prototype.toString ().



A especificação define os valores padrão retornados por Object.prototype.toString () para muitos 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]


Esses tipos não têm uma propriedade Symbol.toStringTag porque o algoritmo Object.prototype.toString () os avalia de uma maneira especial.



A propriedade em questão é definida em tipos como símbolos, funções geradoras, cartas, promessas, etc. Considere um exemplo:



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]


Caso o objeto não seja de um grupo do tipo padrão e não contenha a propriedade @@ toStringTag, o objeto é retornado. Claro, podemos mudar isso:



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]


Uma instância da classe SimpleClass não tem uma propriedade @@ toStringTag, então Object.prototype.toString () retorna [object Object]. O construtor da classe MyTypeClass atribui a propriedade @@ toStringTag à instância com o valor MyType, de forma que Object.prototype.toString () retorna [object MyType].



Observe que @@ toStringTag foi introduzido para compatibilidade com versões anteriores. Seu uso é indesejável. É melhor usar instanceof (junto com @@ hasInstance) ou typeof para determinar o tipo de um objeto.



6. @@ espécies para criar um objeto derivado



Use Symbol.species para definir uma propriedade cujo valor é uma função construtora usada para criar objetos derivados.



O valor @@ species de muitos construtores são os próprios construtores:



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


Primeiro, observe que um objeto derivado é um objeto que é retornado após a realização de uma operação específica no objeto original. Por exemplo, chamar map () retorna um objeto derivado - o resultado da transformação dos elementos do array.



Normalmente, os objetos derivados referem-se ao mesmo construtor dos objetos originais. Mas às vezes torna-se necessário definir outro construtor (talvez uma das classes padrão): é aqui que @@ species pode ajudar.



Suponha que estendamos o construtor Array com a classe filha MyArray para adicionar alguns métodos úteis. Ao fazer isso, queremos que o construtor dos objetos derivados da instância MyArray seja Array. Para fazer isso, você precisa definir uma propriedade computada @@ species com um 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 a propriedade estática computada Symbol.species. Ele especifica que o construtor para objetos derivados deve ser o construtor Array. Posteriormente, ao filtrar os elementos do array, array.filter () retorna Array.



A propriedade computada @@ species é usada por métodos de array e array digitados como map (), concat (), slice (), splice (), que retornam objetos derivados. Usar essa propriedade pode ser útil para estender mapas, expressões regulares ou promessas, preservando o construtor original.



7. Crie uma expressão regular na forma de um objeto: @@ match, @@ replace, @@ search e @@ split



O protótipo da string contém 4 métodos que usam expressões regulares como argumento:



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


ES6 permite que esses métodos aceitem outros tipos definindo as propriedades computadas correspondentes: @@ match, @@ replace, @@ search e @@ split.



Curiosamente, o protótipo RegExp contém os métodos especificados, também definidos por meio de 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


No exemplo a seguir, definimos uma classe que pode ser usada no lugar de uma expressão 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) // ['', '']


A classe Expression define os métodos @@ match, @@ replace, @@ search e @@ split. Em seguida, uma instância desta classe - sunExp é usada nos métodos apropriados em vez de uma expressão regular.



8. @@ isConcatSpreadable para converter objeto em array



Symbol.isConcatSpreadable é um valor booleano que indica que um objeto pode ser convertido em uma matriz usando o método Array.prototype.concat ().



Por padrão, o método concat () recupera os elementos do array (decompõe o array nos elementos que o constituem) ao concatenar os arrays:



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


Para concatenar as duas matrizes, passe letras como um argumento para o método concat (). Os elementos da matriz de letras tornam-se parte do resultado da união: ['c', 'd', 'e', ​​'a', 'b'].



Para evitar que a matriz seja decomposta em elementos e torná-la parte do resultado da união como está, a propriedade @@ isConcatSpreadable deve ser definida como falsa:



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


Em contraste com uma matriz, o método concat () não decompõe objetos semelhantes a uma matriz em elementos. Esse comportamento também pode ser alterado com @@ 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 acessar propriedades com



Symbol.unscopables é uma propriedade computada cujos nomes próprios são excluídos do objeto adicionado ao início da cadeia de escopo usando a instrução with. A propriedade @@ unscopables tem o seguinte formato: {propertyName: <boolean_exclude_binding>}.



ES6 define @@ unscopables apenas para arrays. Isso é feito para ocultar novos métodos que podem substituir variáveis ​​com o mesmo nome no código antigo:



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 acessar o método concat () no corpo com, uma vez que este método não está contido na propriedade @@ unscopables. O método entries () é especificado nesta propriedade e é definido como true, o que o torna indisponível com.



@@ unscopables foi introduzido apenas para compatibilidade com versões anteriores de código legado usando a instrução with (obsoleta e proibida no modo estrito).



Espero que você tenha encontrado algo interessante para você. Obrigado pela atenção.



All Articles