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.