Menu

Symbols em JavaScript: chaves únicas e Symbol.iterator

Entenda o que são symbols em JavaScript, por que eles existem e como usar well-known symbols como Symbol.iterator para deixar seus objetos iteráveis.

Symbol: um valor garantidamente único em JavaScript

O Symbol é um dos tipos primitivos do JavaScript, ao lado de string, number, boolean, null, undefined e bigint. Sua característica principal é bem direta: todo symbol que você cria é diferente de qualquer outro symbol, para sempre.

index.js
Output
Click Run to see the output here.

Repare: dois symbols, ambos criados sem nenhum argumento, e mesmo assim não são iguais. Isso não é um detalhe de implementação — é exatamente a intenção por trás do tipo primitivo symbol. Você não consegue forjar um symbol, recriá-lo por acidente nem colidir com o de outra pessoa.

Dá pra anexar uma descrição para facilitar o debug. Ela não interfere na identidade do symbol:

index.js
Output
Click Run to see the output here.

Mesma descrição, símbolos diferentes. A descrição serve só como um rótulo para humanos lerem nos logs.

Por que Symbols existem: chaves que não colidem

O principal motivo de existir o tipo primitivo symbol em JavaScript é permitir que você adicione propriedades a um objeto sem se preocupar se a sua chave vai bater com a de outra pessoa. Chaves do tipo string disputam um namespace único — se duas bibliotecas resolvem guardar metadados em obj.meta, uma atropela a outra. Com symbol como chave de objeto, isso não acontece, porque ninguém mais tem o seu symbol.

index.js
Output
Click Run to see the output here.

Os colchetes em [ID]: 42 fazem parte da sintaxe de propriedade computada — eles dizem "use o valor de ID como chave". Só quem tiver em mãos esse mesmo symbol ID consegue ler ou sobrescrever essa propriedade. Qualquer outro módulo que use a string "id" ou crie seu próprio Symbol("id") está falando de um espaço totalmente diferente.

Symbol como chave de objeto é (quase) invisível

Propriedades com chave do tipo symbol não aparecem em for...in, Object.keys nem JSON.stringify. Não é que sejam secretas — um código determinado ainda consegue encontrá-las — mas elas ficam fora do caminho durante a iteração normal.

index.js
Output
Click Run to see the output here.

Object.keys e JSON.stringify ignoram completamente a chave do tipo Symbol. Quando você realmente precisa encontrar propriedades baseadas em symbol, dá pra usar Object.getOwnPropertySymbols e Reflect.ownKeys, que as expõem. Esse é justamente o comportamento ideal para metadados — ficam visíveis quando você vai atrás, mas invisíveis para qualquer código que esteja só percorrendo chaves em string.

Compartilhando symbols com Symbol.for

Symbol() cria um symbol local e único, isolado. Mas em alguns casos você quer o contrário: um symbol que seja o mesmo em qualquer ponto do programa, inclusive entre módulos diferentes, para que várias partes do código concordem sobre uma mesma chave. É aí que entra o Symbol.for.

index.js
Output
Click Run to see the output here.

Symbol.for(key) consulta um registro global: se já existe um symbol associado àquela chave, ele é devolvido; caso contrário, um novo é criado e armazenado. Já Symbol.keyFor faz o caminho inverso — retorna a chave com que um symbol registrado foi criado (ou undefined, se ele não estiver no registro).

Na maior parte dos casos, Symbol() puro já resolve. Use Symbol.for só quando o symbol precisa, de fato, ser compartilhado entre módulos diferentes.

Well-known symbols: ganchos da própria linguagem

É aqui que os symbols mostram para que vieram. O JavaScript expõe um conjunto de symbols predefinidos — os chamados well-known symbols — que a própria linguagem procura nos seus objetos. Basta implementar um método usando uma dessas chaves e seu objeto se conecta direto a um recurso nativo.

O mais útil deles é o Symbol.iterator. Qualquer objeto com um método nessa chave vira um objeto iterável em JavaScript: funciona com for...of, spread, desestruturação e Array.from.

index.js
Output
Click Run to see the output here.

range não é um array. É um objeto comum, com um único método que usa uma chave especial. Mas, como esse método retorna um iterador, tanto for...of quanto o spread funcionam nele — do mesmo jeito que funcionam em arrays, strings e Map. Esse é o contrato: basta implementar Symbol.iterator que a linguagem passa a tratar o seu objeto como iterável.

Outros well-known symbols que vale a pena conhecer

Existem alguns outros, cada um servindo de gancho para uma parte diferente da linguagem:

index.js
Output
Click Run to see the output here.
  • Symbol.iterator — torna um objeto iterável.
  • Symbol.asyncIterator — mesma ideia, só que para for await...of.
  • Symbol.toPrimitive — controla como um objeto é convertido em primitivo (número, string ou default) em contextos de coerção.
  • Symbol.hasInstance — personaliza o que o instanceof devolve para a sua classe.
  • Symbol.toStringTag — define a tag que aparece em Object.prototype.toString.call(obj).

Não precisa decorar essa lista. Basta saber que eles existem — quando você quiser que seu objeto se comporte como um tipo nativo, provavelmente há um well-known symbol pronto pra isso.

Symbol não é string

Uma pegadinha clássica: symbols não são convertidos automaticamente em string. Se você tentar concatenar, dá erro. O mesmo acontece se passar um symbol para uma API que espera uma string:

index.js
Output
Click Run to see the output here.

Template literals disparam o mesmo TypeError. Se você realmente quiser texto, use String(symbol) ou symbol.toString(). Essas arestas são propositais — a linguagem está te protegendo de tratar, sem querer, um valor de identidade única como se fosse mais um pedaço de string.

Quando usar Symbols (e quando não usar)

Vale a pena recorrer a symbols quando:

  • Você precisa anexar metadados em objetos que não são seus e quer uma chave que não vá colidir com nada.
  • Você está projetando um protocolo — algo como "objetos que quiserem se integrar à minha lib devem implementar um método nesta chave symbol".
  • Você quer uma propriedade que não vaze em JSON.stringify nem em for...in.
  • Você está implementando Symbol.iterator ou outro well-known symbol.

Evite symbols quando:

  • Você só precisa de uma chave de objeto e não há risco de colisão. Uma string é mais simples e aparece nos logs como ela mesma.
  • Você quer campos "privados". Propriedades com chave symbol não são realmente privadas — Object.getOwnPropertySymbols acha todas. Para privacidade de verdade, use campos de classe com #private.
  • Você precisa guardar dados que sobrevivam a um JSON.stringify. Não vão sobreviver.

A maior parte do código JavaScript vive tranquilamente sem nunca escrever Symbol(...) diretamente. Mas no momento em que você quer que um objeto seu funcione com for...of ou se comporte de forma natural num contexto de coerção, os symbols são a porta de entrada para esse mecanismo.

A seguir: declarações de função

Symbols, iteradores e generators se apoiam pesado em funções — os métodos armazenados em Symbol.iterator, as factories que retornam iteradores, os generators cuja forma function* esconde boa parte do boilerplate. Funções são o próximo capítulo, começando pelas diferentes maneiras de declará-las e o que muda entre cada forma.

Perguntas frequentes

O que é um symbol em JavaScript?

Symbol é um tipo primitivo cujos valores são sempre únicos. Você cria um com Symbol() ou Symbol('descrição'), e duas chamadas nunca retornam symbols iguais. Na prática, eles são usados como chaves de objeto que não colidem com outras chaves e como "ganchos" para recursos da linguagem, via well-known symbols como o Symbol.iterator.

Quando vale a pena usar symbol em vez de string como chave?

Use symbol quando precisar adicionar uma propriedade a um objeto sem correr o risco de conflitar com outro código — bibliotecas anexando metadados, frameworks marcando objetos, ou quando você quer definir uma chave que não faz parte da API pública. Para chaves do dia a dia, onde a legibilidade importa e colisão não é um problema, strings continuam sendo a melhor escolha.

Para que serve o Symbol.iterator?

Symbol.iterator é um well-known symbol que diz ao for...of, ao spread e à desestruturação como iterar um objeto. Se você define um método na chave Symbol.iterator que retorna um iterator, seu objeto passa a ser iterável. É exatamente assim que arrays, strings, Map e Set funcionam por baixo dos panos.

Qual a diferença entre Symbol() e Symbol.for()?

Symbol('x') cria um symbol novo toda vez — duas chamadas com a mesma descrição continuam sendo symbols diferentes. Já Symbol.for('x') consulta um registro global e devolve o mesmo symbol para a mesma chave em qualquer lugar do programa. Use Symbol.for quando precisar compartilhar symbols entre módulos ou realms; use Symbol() para unicidade local.

Aprenda a programar com o Coddy

COMEÇAR