Você sabia que os elementos DOM com IDs são acessíveis em JavaScript como variáveis globais? É uma daquelas coisas que existe desde sempre, mas estou realmente investigando isso pela primeira vez.
Se esta é a primeira vez que você ouve falar sobre isso, prepare-se! Podemos ver isso em ação simplesmente adicionando um ID a um elemento em HTML:
Normalmente, definiríamos uma nova variável usando querySelector("#cool")
or getElementById("cool")
para selecionar esse elemento:
var el = querySelector("#cool");
Mas na verdade já temos acesso a #cool
sem aquele rigamorale:
Então, qualquer id
- ou name
atributo, aliás - no HTML pode ser acessado em JavaScript usando window[ELEMENT_ID]
. Novamente, isso não é exatamente “novo”, mas é realmente incomum de ver.
Como você pode imaginar, acessar o escopo global com referências nomeadas não é uma boa ideia. Algumas pessoas passaram a chamar isso de “poluidor de âmbito global”. Veremos por que isso acontece, mas primeiro…
Algum contexto
Essa abordagem é descrito na especificação HTML, onde é descrito como “acesso nomeado no Window
objeto."
O Internet Explorer foi o primeiro a implementar o recurso. Todos os outros navegadores também o adicionaram. Gecko era o único navegador na época que não o suportava diretamente no modo padrão, optando por torná-lo um recurso experimental. Houve hesitação em implementá-lo, mas avançou em nome da compatibilidade do navegador (Gecko até tentou convencer o WebKit para movê-lo para fora do modo padrão) e finalmente chegou ao modo padrão no Firefox 14.
Uma coisa que pode não ser bem conhecida é que os navegadores tiveram que implementar algumas medidas de precaução – com graus variados de sucesso – para garantir que os globais gerados não quebrassem a página da web. Uma dessas medidas é…
Sombreamento variável
Provavelmente a parte mais interessante deste recurso é que as referências de elementos nomeados não sombrear variáveis globais existentes. Então, se um elemento DOM tiver um id
que já está definido como global, não substituirá o existente. Por exemplo:
window.foo = "bar";
I won't override window.foo
console.log(window.foo); // Prints "bar"
E o oposto também é verdadeiro:
I will be overridden :(
window.foo = "bar";
console.log(window.foo); // Prints "bar"
Este comportamento é essencial porque anula substituições perigosas, como
, o que de outra forma criaria um conflito ao invalidar o alert
API. Essa técnica de proteção pode muito bem ser a razão pela qual você – se você é como eu – está aprendendo sobre isso pela primeira vez.
O caso contra globais nomeados
Anteriormente, eu disse que usar elementos nomeados globais como referências pode não ser a melhor ideia. Existem muitas razões para isso, que TJ VanToll cobriu muito bem em seu blog e vou resumir aqui:
- Se o DOM mudar, a referência também mudará. Isso torna alguns realmente “frágeis” (o termo da especificação para isso) código onde a separação de interesses entre HTML e JavaScript pode ser demais.
- Referências acidentais são fáceis demais. Um simples erro de digitação pode muito bem acabar fazendo referência a um nome global e gerar resultados inesperados.
- Ele é implementado de forma diferente nos navegadores. Por exemplo, deveríamos ser capazes de acessar uma âncora com um
id
- por exemplo- mas alguns navegadores (nomeadamente Safari e Firefox) retornam um
ReferenceError
no console. - Pode não retornar o que você pensa. De acordo com a especificação, quando há múltiplas instâncias do mesmo elemento nomeado no DOM — digamos, duas instâncias de
— o navegador deve retornar um
HTMLCollection
com uma matriz de instâncias. O Firefox, entretanto, retorna apenas a primeira instância. Então de novo, a especificação diz devemos usar uma instância de umid
na árvore de um elemento de qualquer maneira. Mas fazer isso não impedirá o funcionamento de uma página ou algo parecido. - Talvez haja um custo de desempenho? Quero dizer, o navegador precisa fazer essa lista de referências e mantê-la. Algumas pessoas fizeram testes neste tópico StackOverflow, onde os globais nomeados eram na verdade mais desempenho em um teste e menos desempenho em um teste mais recente.
Considerações adicionais
Digamos que descartamos as críticas contra o uso de globais nomeados e os usamos de qualquer maneira. É tudo de bom. Mas há algumas coisas que você pode querer considerar ao fazer isso.
Polifills
Por mais extremo que possa parecer, esses tipos de verificações globais são um requisito típico de configuração para polyfills. Confira o exemplo a seguir, onde definimos um cookie usando o novo CookieStore
API, preenchendo-o em navegadores que ainda não o suportam:
// Polyfill the CookieStore API if not yet implemented.
// https://developer.mozilla.org/en-US/docs/Web/API/CookieStore
if (!window.cookieStore) {
window.cookieStore = myCookieStorePolyfill;
}
cookieStore.set("foo", "bar");
Este código funciona perfeitamente no Chrome, mas gera o seguinte erro no Safari:
TypeError: cookieStore.set is not a function
O Safari não tem suporte para o CookieStore
API no momento desta redação. Como resultado, o polyfill não é aplicado porque o img
ID do elemento cria uma variável global que entra em conflito com o cookieStore
global.
Atualizações da API JavaScript
Podemos inverter a situação e encontrar outro problema em que as atualizações no mecanismo JavaScript do navegador podem quebrar as referências globais de um elemento nomeado.
Por exemplo:
window.BarcodeDetector.focus();
Esse script pega uma referência ao elemento input e invoca focus()
nele. Funciona corretamente. Ainda assim, não sabemos como longo ele continuará funcionando.
Veja, a variável global que estamos usando para referenciar o elemento input irá parar de funcionar assim que os navegadores começarem a suportar o BarcodeDetector
API. Nesse ponto, o window.BarcodeDetector
global não será mais uma referência ao elemento de entrada e .focus()
vai lançar um “window.BarcodeDetector.focus
não é um erro de função”.
Bônus: nem todos os elementos nomeados geram referências globais
Queres ouvir uma coisa engraçada? Para piorar ainda mais a situação, os elementos nomeados são acessíveis como variáveis globais apenas se os nomes contiverem apenas letras. Os navegadores não criarão uma referência global para um elemento com um ID que contenha caracteres especiais e números, como hello-world
e item1
.
Conclusão
Vamos resumir como chegamos aqui:
- Todos os principais navegadores criam automaticamente referências globais para cada elemento DOM com um
id
(ou, em alguns casos, umname
atributo). - Acessar esses elementos através de suas referências globais não é confiável e é potencialmente perigoso. Usar
querySelector
orgetElementById
ao invés. - Como as referências globais são geradas automaticamente, elas podem ter alguns efeitos colaterais no seu código. Essa é uma boa razão para evitar usar o
id
atributo, a menos que você realmente precise dele.
No final das contas, provavelmente é uma boa ideia evitar o uso de globais nomeados em JavaScript. Eu citei a especificação anteriormente sobre como isso leva a um código “frágil”, mas aqui está o texto completo para deixar claro:
Como regra geral, confiar nisso levará a um código frágil. Quais IDs acabam mapeados para esta API podem variar ao longo do tempo, à medida que novos recursos são adicionados à plataforma web, por exemplo. Em vez disso, use
document.getElementById()
ordocument.querySelector()
.
Acho que o fato de a própria especificação HTML recomendar ficar longe desse recurso fala por si.