Symbol é um tipo de dado primitivo introduzido no ECMAScript 2015 (ES6) que representa um identificador único e imutável. Diferente de strings ou números, cada chamada à função Symbol() gera um valor absolutamente único, mesmo que receba a mesma descrição. Isso permite criar propriedades de objetos que não colidem com outras propriedades, fundamentais para bibliotecas e para a metaprogramação em JavaScript.

Nesta aula, vamos explorar o que são Symbols, por que foram criados, os Symbols bem conhecidos que a linguagem oferece e como usá-los na prática para resolver problemas reais, como evitar colisões de nomes e personalizar comportamentos nativos.

O que é

Um Symbol é um valor primitivo criado pela função Symbol(). Ele é único por natureza: dois Symbols nunca são iguais, mesmo que tenham a mesma descrição. A descrição é apenas uma etiqueta para depuração, não afeta a identidade do Symbol.

Exemplo básico:

const simb1 = Symbol('descricao');
const simb2 = Symbol('descricao');
console.log(simb1 === simb2); // false
console.log(typeof simb1); // "symbol"

Além disso, Symbols podem ser usados como chaves de propriedades de objetos. Quando usados como chaves, eles não são enumerados em loops for...in nem em Object.keys(), mas podem ser acessados diretamente via colchetes ou com Object.getOwnPropertySymbols().

Também existe o Symbol.for() e Symbol.keyFor(), que criam e acessam Symbols em um registro global, permitindo compartilhar o mesmo Symbol entre diferentes partes do código.

Por que existe

Antes dos Symbols, as chaves de propriedades de objetos eram sempre strings. Isso poderia causar colisões acidentais, especialmente quando bibliotecas diferentes usavam nomes comuns como "id" ou "tipo". Symbols resolvem esse problema garantindo unicidade absoluta, sem depender de convenções de nomenclatura.

Além disso, Symbols permitem adicionar propriedades "escondidas" a objetos de terceiros sem risco de sobrescrever propriedades existentes. Por exemplo, uma biblioteca pode anexar metadados a um objeto sem que o usuário perceba ou interfira.

Outra motivação é a metaprogramação: a especificação ECMAScript define vários Symbols bem conhecidos que permitem personalizar o comportamento de objetos em operações nativas, como Symbol.iterator para iteração e Symbol.toPrimitive para conversão de tipos.

Symbols bem conhecidos

O JavaScript fornece um conjunto de Symbols pré-definidos, chamados de "well-known symbols" (símbolos bem conhecidos), que são propriedades estáticas da função Symbol. Eles são usados pela engine para invocar métodos específicos em objetos.

Alguns dos mais importantes:

  • Symbol.iterator: usado para definir o iterador padrão de um objeto. É chamado quando usamos for...of ou spread.
  • Symbol.toPrimitive: controla como o objeto é convertido para um valor primitivo (número, string, boolean).
  • Symbol.toStringTag: personaliza a descrição retornada por Object.prototype.toString().
  • Symbol.hasInstance: personaliza o comportamento do operador instanceof.
  • Symbol.species: permite que métodos como map() e filter() criem objetos derivados de um tipo específico.

Exemplo com Symbol.iterator:

const obj = {
  *[Symbol.iterator]() {
    yield 1;
    yield 2;
    yield 3;
  }
};

for (const valor of obj) {
  console.log(valor); // 1, 2, 3
}

Exemplo com Symbol.toPrimitive:

const numero = {
  [Symbol.toPrimitive](hint) {
    if (hint === 'number') return 42;
    if (hint === 'string') return 'quarenta e dois';
    return null;
  }
};
console.log(+numero); // 42
console.log(String(numero)); // "quarenta e dois"

Uso prático

Na prática, Symbols são muito úteis em bibliotecas e frameworks para criar propriedades privadas ou semi-privadas, evitar conflitos de nomes e implementar protocolos personalizados. Por exemplo, podemos criar um sistema de eventos onde cada tipo de evento é um Symbol:

const Evento = {
  clique: Symbol('clique'),
  teclado: Symbol('teclado')
};

function registrarListener(elemento, tipoEvento, callback) {
  // armazena callback em um mapa interno
  elemento[Symbol.for('listeners')] = elemento[Symbol.for('listeners')] || {};
  elemento[Symbol.for('listeners')][tipoEvento] = callback;
}

// Uso
registrarListener(meuBotao, Evento.clique, () => console.log('clicou'));

Outro uso comum é para definir propriedades não enumeráveis, como no exemplo de metadados:

const METADATA = Symbol('metadata');
const user = { nome: 'João' };
user[METADATA] = { criadoEm: new Date() };

console.log(Object.keys(user)); // ["nome"]
console.log(user[METADATA]); // { criadoEm: ... }

Além disso, Symbols são essenciais para implementar iteradores personalizados, como em coleções próprias, e para definir comportamentos de conversão de tipos em objetos complexos.

Boas práticas e observações

  • Use Symbols para criar chaves únicas e não enumeráveis quando precisar de privacidade ou isolamento.
  • Prefira Symbol.for() quando precisar compartilhar o mesmo Symbol globalmente (por exemplo, entre módulos).
  • Não use Symbols como chaves em objetos que serão serializados com JSON, pois JSON.stringify() ignora propriedades com chave Symbol.
  • Para depuração, sempre forneça uma descrição ao criar Symbols.
  • Lembre-se de que Symbols não são convertidos implicitamente para strings, então use String(symbol) ou symbol.description quando precisar exibir.

Referências

Exercícios

  1. Exercício 1: Crie dois Symbols com a mesma descrição e verifique se são iguais. Depois, crie um objeto e use um deles como chave de uma propriedade. Imprima o valor da propriedade e as chaves do objeto com Object.keys().
  2. ✓ Resposta:
    const s1 = Symbol('teste');
    const s2 = Symbol('teste');
    console.log(s1 === s2); // false
    
    const obj = {};
    obj[s1] = 'valor';
    console.log(obj[s1]); // "valor"
    console.log(Object.keys(obj)); // []
  3. Exercício 2: Use Symbol.for() para criar um Symbol global e recuperá-lo em outro lugar. Verifique se são a mesma referência. Em seguida, use Symbol.keyFor() para obter a chave.
  4. ✓ Resposta:
    const global1 = Symbol.for('minhaChave');
    const global2 = Symbol.for('minhaChave');
    console.log(global1 === global2); // true
    console.log(Symbol.keyFor(global1)); // "minhaChave"
  5. Exercício 3: Implemente um objeto que seja iterável usando Symbol.iterator para produzir os números pares de 0 a 10.
  6. ✓ Resposta:
    const pares = {
      [Symbol.iterator]() {
        let num = 0;
        return {
          next() {
            if (num > 10) return { done: true };
            const value = num;
            num += 2;
            return { value, done: false };
          }
        };
      }
    };
    
    for (const n of pares) {
      console.log(n); // 0, 2, 4, 6, 8, 10
    }
  7. Exercício 4: Crie um objeto que, ao ser convertido para string, retorne "[objeto Especial]" usando Symbol.toStringTag.
  8. ✓ Resposta:
    const obj = {
      [Symbol.toStringTag]: 'Especial'
    };
    console.log(Object.prototype.toString.call(obj)); // "[object Especial]"
  9. Exercício 5: Crie uma função que receba um objeto e retorne um novo objeto com as mesmas propriedades enumeráveis, mas sem as propriedades cujas chaves sejam Symbols. Use Object.getOwnPropertySymbols() para identificar.
  10. ✓ Resposta:
    function removerSymbols(obj) {
      const novo = {};
      for (const chave in obj) {
        if (typeof chave !== 'symbol') {
          novo[chave] = obj[chave];
        }
      }
      return novo;
    }
    
    // Ou usando Object.keys:
    function removerSymbols2(obj) {
      const novo = {};
      Object.keys(obj).forEach(chave => novo[chave] = obj[chave]);
      return novo;
    }