Por especificação, apenas dois tipos primitivos podem servir como chaves de propriedade de objeto:
tipo de string ou
tipo de símbolo.
Caso contrário, se alguém usar outro tipo, como número, ele será automaticamente convertido em string. Portanto, obj[1]
é o mesmo que obj["1"]
, e obj[true]
é o mesmo que obj["true"]
.
Até agora usamos apenas strings.
Agora vamos explorar os símbolos, ver o que eles podem fazer por nós.
Um “símbolo” representa um identificador exclusivo.
Um valor deste tipo pode ser criado usando Symbol()
:
deixe id = Símbolo();
Após a criação, podemos fornecer uma descrição aos símbolos (também chamada de nome do símbolo), útil principalmente para fins de depuração:
// id é um símbolo com a descrição "id" deixe id = Símbolo("id");
Os símbolos são garantidos como únicos. Mesmo se criarmos muitos símbolos com exatamente a mesma descrição, eles terão valores diferentes. A descrição é apenas um rótulo que não afeta nada.
Por exemplo, aqui estão dois símbolos com a mesma descrição – eles não são iguais:
deixe id1 = Símbolo("id"); deixe id2 = Símbolo("id"); alerta(id1 == id2); // falso
Se você estiver familiarizado com Ruby ou outra linguagem que também possua algum tipo de “símbolo” – por favor, não se engane. Os símbolos JavaScript são diferentes.
Então, para resumir, um símbolo é um “valor primitivo único” com uma descrição opcional. Vamos ver onde podemos usá-los.
Os símbolos não são convertidos automaticamente em uma string
A maioria dos valores em JavaScript suporta conversão implícita em uma string. Por exemplo, podemos alert
quase qualquer valor e funcionará. Os símbolos são especiais. Eles não convertem automaticamente.
Por exemplo, este alert
mostrará um erro:
deixe id = Símbolo("id"); alerta(id); // TypeError: Não é possível converter um valor de símbolo em uma string
Isso é uma “proteção de linguagem” contra confusão, porque strings e símbolos são fundamentalmente diferentes e não devem ser convertidos acidentalmente um no outro.
Se realmente quisermos mostrar um símbolo, precisamos chamar explicitamente .toString()
nele, como aqui:
deixe id = Símbolo("id"); alerta(id.toString()); // Símbolo(id), agora funciona
Ou obtenha a propriedade symbol.description
para mostrar apenas a descrição:
deixe id = Símbolo("id"); alerta(id.descrição); // eu ia
Os símbolos nos permitem criar propriedades “ocultas” de um objeto, que nenhuma outra parte do código pode acessar ou substituir acidentalmente.
Por exemplo, se estivermos trabalhando com objetos user
que pertencem a um código de terceiros. Gostaríamos de adicionar identificadores a eles.
Vamos usar uma chave de símbolo para isso:
let user = { // pertence a outro código nome: "João" }; deixe id = Símbolo("id"); usuário[id] = 1; alerta(usuário[id]); // podemos acessar os dados usando o símbolo como chave
Qual é a vantagem de usar Symbol("id")
sobre uma string "id"
?
Como os objetos user
pertencem a outra base de código, não é seguro adicionar campos a eles, pois podemos afetar o comportamento predefinido nessa outra base de código. No entanto, os símbolos não podem ser acessados acidentalmente. O código de terceiros não terá conhecimento dos símbolos recém-definidos, por isso é seguro adicionar símbolos aos objetos user
.
Além disso, imagine que outro script queira ter seu próprio identificador dentro de user
, para seus próprios propósitos.
Então esse script pode criar seu próprio Symbol("id")
, assim:
// ... deixe id = Símbolo("id"); user[id] = "Seu valor de id";
Não haverá conflito entre os nossos identificadores e seus identificadores, pois os símbolos são sempre diferentes, mesmo que tenham o mesmo nome.
…Mas se usássemos uma string "id"
em vez de um símbolo para o mesmo propósito, haveria um conflito:
deixe usuário = {nome: "John" }; // Nosso script usa a propriedade "id" user.id = "Nosso valor de id"; // ...Outro script também quer "id" para seus propósitos... user.id = "Seu valor de id" // Bum! substituído por outro script!
Se quisermos usar um símbolo em um objeto literal {...}
, precisaremos de colchetes ao redor dele.
Assim:
deixe id = Símbolo("id"); deixe usuário = { nome: "João", [id]: 123 // não "id": 123 };
Isso porque precisamos do valor da variável id
como chave, não da string “id”.
Propriedades simbólicas não participam do loop for..in
.
Por exemplo:
deixe id = Símbolo("id"); deixe usuário = { nome: "João", idade: 30, [id]: 123 }; for (deixar digitar o usuário) alert(key); //nome, idade (sem símbolos) // o acesso direto pelo símbolo funciona alert("Direto: " + usuário[id]); // Direto: 123
Object.keys(user) também os ignora. Isso faz parte do princípio geral de “ocultar propriedades simbólicas”. Se outro script ou biblioteca fizer um loop sobre nosso objeto, ele não acessará inesperadamente uma propriedade simbólica.
Por outro lado, Object.assign copia propriedades de string e símbolo:
deixe id = Símbolo("id"); deixe usuário = { [id]: 123 }; deixe clonar = Object.assign({}, usuário); alerta(clone[id]); //123
Não há paradoxo aqui. Isso é intencional. A ideia é que quando clonamos um objeto ou mesclamos objetos, geralmente queremos que todas as propriedades sejam copiadas (incluindo símbolos como id
).
Como vimos, normalmente todos os símbolos são diferentes, mesmo que tenham o mesmo nome. Mas às vezes queremos que símbolos com o mesmo nome sejam as mesmas entidades. Por exemplo, diferentes partes da nossa aplicação desejam acessar o símbolo "id"
que significa exatamente a mesma propriedade.
Para conseguir isso, existe um registro global de símbolos . Podemos criar símbolos nele e acessá-los posteriormente, e isso garante que acessos repetidos com o mesmo nome retornem exatamente o mesmo símbolo.
Para ler (criar se estiver ausente) um símbolo do registro, use Symbol.for(key)
.
Essa chamada verifica o registro global e, se houver um símbolo descrito como key
, o retorna; caso contrário, cria um novo símbolo Symbol(key)
e o armazena no registro pela key
fornecida.
Por exemplo:
//lê do registro global deixe id = Símbolo.for("id"); // se o símbolo não existia, ele é criado // leia novamente (talvez de outra parte do código) deixe idAgain = Symbol.for("id"); //o mesmo símbolo alert(id === idNovamente); // verdadeiro
Os símbolos dentro do registro são chamados de símbolos globais . Se quisermos um símbolo para todo o aplicativo, acessível em qualquer parte do código – é para isso que eles servem.
Isso soa como Ruby
Em algumas linguagens de programação, como Ruby, existe um único símbolo por nome.
Em JavaScript, como podemos ver, isso vale para símbolos globais.
Vimos que para símbolos globais, Symbol.for(key)
retorna um símbolo por nome. Para fazer o oposto – retornar um nome por símbolo global – podemos usar: Symbol.keyFor(sym)
:
Por exemplo:
//obtém o símbolo pelo nome deixe sym = Symbol.for("nome"); deixe sym2 = Símbolo.for("id"); //obtém o nome por símbolo alerta(Símbolo.keyFor(sym)); // nome alerta(Símbolo.keyFor(sym2)); // eu ia
O Symbol.keyFor
usa internamente o registro global de símbolos para procurar a chave do símbolo. Portanto, não funciona para símbolos não globais. Se o símbolo não for global, não será possível encontrá-lo e retornará undefined
.
Dito isto, todos os símbolos possuem a propriedade description
.
Por exemplo:
deixe globalSymbol = Symbol.for("nome"); deixe localSymbol = Símbolo("nome"); alerta(Símbolo.keyFor(globalSymbol)); //nome, símbolo global alerta(Símbolo.keyFor(localSymbol)); // indefinido, não global alerta(localSymbol.descrição); // nome
Existem muitos símbolos de “sistema” que o JavaScript usa internamente e podemos usá-los para ajustar vários aspectos de nossos objetos.
Eles estão listados na especificação da tabela de símbolos conhecidos:
Symbol.hasInstance
Symbol.isConcatSpreadable
Symbol.iterator
Symbol.toPrimitive
…e assim por diante.
Por exemplo, Symbol.toPrimitive
nos permite descrever objetos para conversão primitiva. Veremos seu uso muito em breve.
Outros símbolos também se tornarão familiares quando estudarmos as características linguísticas correspondentes.
Symbol
é um tipo primitivo para identificadores exclusivos.
Os símbolos são criados com a chamada Symbol()
com uma descrição opcional (nome).
Os símbolos são sempre valores diferentes, mesmo que tenham o mesmo nome. Se quisermos que os símbolos com o mesmo nome sejam iguais, devemos usar o registro global: Symbol.for(key)
retorna (cria se necessário) um símbolo global com key
como nome. Várias chamadas de Symbol.for
com a mesma key
retornam exatamente o mesmo símbolo.
Os símbolos têm dois casos de uso principais:
Propriedades do objeto “oculto”.
Se quisermos adicionar uma propriedade em um objeto que “pertence” a outro script ou biblioteca, podemos criar um símbolo e usá-lo como chave de propriedade. Uma propriedade simbólica não aparece for..in
, portanto não será processada acidentalmente junto com outras propriedades. Também não será acessado diretamente, pois outro script não possui nosso símbolo. Assim, a propriedade estará protegida contra uso acidental ou substituição.
Assim, podemos ocultar “discretamente” algo em objetos que precisamos, mas que outros não deveriam ver, usando propriedades simbólicas.
Existem muitos símbolos de sistema usados por JavaScript que são acessíveis como Symbol.*
. Podemos usá-los para alterar alguns comportamentos internos. Por exemplo, mais adiante no tutorial usaremos Symbol.iterator
para iteráveis, Symbol.toPrimitive
para configurar a conversão de objeto em primitivo e assim por diante.
Tecnicamente, os símbolos não estão 100% ocultos. Existe um método integrado Object.getOwnPropertySymbols(obj) que nos permite obter todos os símbolos. Também existe um método chamado Reflect.ownKeys(obj) que retorna todas as chaves de um objeto, incluindo as simbólicas. Mas a maioria das bibliotecas, funções integradas e construções de sintaxe não usam esses métodos.