Symbol
Nesta aula, você aprenderá sobre o tipo Symbol do JavaScript, sua finalidade, os symbols bem conhecidos e aplicações práticas. Vamos explorar como Symbols criam propriedades únicas e controlam comportamentos internos de objetos.
O tipo Symbol foi introduzido no ECMAScript 6 (ES6) e representa um valor primitivo único e imutável. Cada chamada da função Symbol() retorna um novo símbolo, garantindo que ele nunca seja igual a outro, mesmo que tenha a mesma descrição. Essa unicidade é a base para várias aplicações importantes no JavaScript moderno, como a criação de propriedades de objetos que não colidem com outras chaves e a personalização de comportamentos internos de objetos.
Nesta aula, vamos mergulhar no mundo dos Symbols: entender por que eles existem, como usá-los no dia a dia e quais são os chamados "symbols bem conhecidos" que permitem modificar a forma como o JavaScript interage com nossos objetos. Ao final, você terá uma visão clara e prática desse recurso essencial.
O que é
Um Symbol é um valor primitivo criado pela função Symbol(). Diferente de outros primitivos (como string, número e booleano), cada symbol é único e imutável. Isso significa que não existem dois symbols iguais, mesmo que você forneça a mesma descrição (que é apenas um rótulo opcional, usado para depuração).
Exemplo básico:
const sym1 = Symbol('descricao');
const sym2 = Symbol('descricao');
console.log(sym1 === sym2); // false
console.log(sym1.toString()); // "Symbol(descricao)"
console.log(typeof sym1); // "symbol"Symbols podem ser usados como chaves de propriedades de objetos. Quando você define uma propriedade com uma chave do tipo symbol, ela não aparece em iterações normais (como for...in ou Object.keys), mas pode ser acessada diretamente usando o symbol como referência.
const obj = {};
const uniqueKey = Symbol('minhaChave');
obj[uniqueKey] = 'valor secreto';
console.log(obj[uniqueKey]); // 'valor secreto'
console.log(Object.keys(obj)); // []
console.log(Object.getOwnPropertySymbols(obj)); // [ Symbol(minhaChave) ]Essa característica torna os Symbols ideais para criar propriedades "escondidas" ou para evitar colisões em bibliotecas e frameworks, onde vários módulos podem adicionar propriedades a objetos compartilhados sem interferir uns nos outros.
Por que existe
Antes dos Symbols, as chaves de propriedades eram sempre strings. Isso podia causar problemas quando bibliotecas diferentes adicionavam propriedades com o mesmo nome a um objeto, resultando em sobrescrita acidental. Os Symbols resolveram esse problema fornecendo chaves verdadeiramente únicas, impossíveis de colidir.
Além disso, os Symbols permitem que o JavaScript tenha um mecanismo para "meta-programação": você pode personalizar o comportamento de objetos em operações internas, como iteração, conversão de tipo, inspeção de objeto, entre outras, definindo métodos com chaves de symbols bem conhecidos. Isso é fundamental para recursos como Symbol.iterator, que permite que um objeto seja iterável com for...of.
Outro motivo é a privacidade. Embora não seja uma privacidade real (os symbols podem ser descobertos via Object.getOwnPropertySymbols), eles oferecem um nível de encapsulamento que não existia antes, pois as propriedades com chaves symbol não são enumeráveis e não aparecem em APIs como Object.keys ou JSON.stringify.
Em resumo, os Symbols existem para trazer unicidade, evitar colisões e habilitar a personalização de comportamentos internos do JavaScript, tornando a linguagem mais flexível e poderosa.
Symbols bem conhecidos
O JavaScript define vários symbols estáticos, chamados de "well-known symbols", que são propriedades do objeto Symbol. Cada um deles corresponde a um comportamento específico que você pode personalizar em seus objetos. Vamos ver os principais:
Symbol.iterator: usado para definir o iterador padrão de um objeto. Quando você usafor...of, o JavaScript procura esse método.Symbol.toPrimitive: permite controlar como o objeto é convertido em um valor primitivo (número, string, booleano).Symbol.toStringTag: usado para personalizar a descrição de um objeto quandoObject.prototype.toStringé chamado.Symbol.hasInstance: personaliza o comportamento do operadorinstanceof.Symbol.isConcatSpreadable: indica se um objeto deve ser espalhado quando usado comArray.prototype.concat.Symbol.match,Symbol.replace,Symbol.search,Symbol.split: personalizam o comportamento de métodos de string comoString.prototype.match,replace,searchesplit.
Vamos ver um exemplo prático com Symbol.iterator:
const iteravel = {
*[Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
}
};
for (const valor of iteravel) {
console.log(valor); // 1, 2, 3
}Outro exemplo com Symbol.toPrimitive:
const objeto = {
[Symbol.toPrimitive](hint) {
if (hint === 'number') return 42;
if (hint === 'string') return 'quarenta e dois';
return 'default';
}
};
console.log(+objeto); // 42
console.log(String(objeto)); // 'quarenta e dois'
console.log(objeto + ''); // 'default'Esses são apenas alguns exemplos. A lista completa de well-known symbols pode ser encontrada na documentação MDN.
Uso prático
Além dos well-known symbols, os Symbols têm aplicações práticas no desenvolvimento de software:
- Criar propriedades únicas em objetos: especialmente útil em bibliotecas e frameworks para evitar conflitos de nomes.
- Definir metadados: você pode anexar informações a objetos sem poluir o namespace público.
- Simular métodos privados: embora não seja uma privacidade real, os Symbols permitem que métodos não sejam acessíveis via iteração, mas ainda podem ser chamados se você tiver a referência do symbol.
- Implementar protocolos de iteração: como vimos,
Symbol.iteratoré essencial para criar objetos iteráveis personalizados. - Personalizar conversões de tipo: com
Symbol.toPrimitive, você pode controlar como seus objetos são convertidos em números, strings etc.
Vamos criar um exemplo de uma classe que usa Symbols para encapsular dados internos:
const _dados = Symbol('dados');
class Pessoa {
constructor(nome) {
this[_dados] = { nome };
}
get nome() {
return this[_dados].nome;
}
set nome(novoNome) {
this[_dados].nome = novoNome;
}
}
const p = new Pessoa('João');
console.log(p.nome); // 'João'
p.nome = 'Maria';
console.log(p.nome); // 'Maria'
console.log(Object.keys(p)); // []
console.log(Object.getOwnPropertySymbols(p)); // [ Symbol(dados) ]Note que Object.keys(p) retorna um array vazio, pois a propriedade _dados é um symbol e não é enumerável. Isso ajuda a esconder a implementação interna.
Boas práticas
- Use Symbols quando precisar de unicidade absoluta, especialmente em bibliotecas.
- Documente os symbols que você usa, pois eles não são visíveis em ferramentas de inspeção comuns.
- Prefira usar well-known symbols em vez de inventar seus próprios para comportamentos padrão.
- Lembre-se de que Symbols não são strings; não tente concatená-los com strings sem convertê-los explicitamente.
- Use
Symbol.for()eSymbol.keyFor()para registrar symbols globalmente quando precisar compartilhar o mesmo symbol entre diferentes partes do código.
Exercícios
- Crie um symbol chamado
ide use-o como chave de uma propriedade em um objeto. Atribua um valor, depois acesse e imprima o valor. - Implemente um objeto iterável que produza os números pares até 10 usando
Symbol.iterator. - Utilize
Symbol.toPrimitivepara que um objeto seja convertido em string como "valor: 100" e em número como 200. - Explique por que
Object.keys()não retorna propriedades com chaves symbol e como você pode obtê-las. - Crie uma classe que use um symbol para armazenar um atributo privado e forneça métodos get/set públicos.
const id = Symbol('id');
const obj = { [id]: 123 };
console.log(obj[id]); // 123const pares = {
[Symbol.iterator]() {
let atual = 0;
return {
next() {
atual += 2;
if (atual <= 10) {
return { value: atual, done: false };
}
return { done: true };
}
};
}
};
console.log([...pares]); // [2, 4, 6, 8, 10]const obj = {
[Symbol.toPrimitive](hint) {
if (hint === 'string') return 'valor: 100';
if (hint === 'number') return 200;
return null;
}
};
console.log(String(obj)); // 'valor: 100'
console.log(+obj); // 200Object.keys() retorna apenas as propriedades enumeráveis com chaves do tipo string. Symbols não são strings e, por padrão, não são enumeráveis. Para obter as chaves symbol, use Object.getOwnPropertySymbols(obj).const _valor = Symbol('valor');
class Conta {
constructor(saldo) {
this[_valor] = saldo;
}
get saldo() {
return this[_valor];
}
set saldo(novoSaldo) {
if (novoSaldo < 0) throw new Error('Saldo não pode ser negativo');
this[_valor] = novoSaldo;
}
}
const c = new Conta(100);
console.log(c.saldo); // 100
c.saldo = 150;
console.log(c.saldo); // 150