Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIniciantecódigo testado

querySelector e getElementById: selecionar elementos

Como achar um elemento na página com querySelector, getElementById e querySelectorAll — e por que HTMLCollection viva quebra o seu laço de remoção.

Rodolfo Mori11 min de leitura

Para mexer num elemento você precisa primeiro achá-lo. São dois métodos que resolvem 95% dos casos: document.getElementById('cupom'), que busca por id, e document.querySelector('.item .preco'), que aceita qualquer seletor CSS e devolve o primeiro que casar.

A regra curta: querySelector para tudo, getElementById quando o alvo tem id. Os dois devolvem null quando não acham nada — e é esse null que gera o erro mais comum de quem está começando com o DOM.

Todos os exemplos rodam nesta página, que é o carrinho da mesma loja das lições anteriores:

html
<main id="carrinho">
  <h1>Seu carrinho</h1>
  <ul class="itens">
    <li class="item" data-id="1"><span class="nome">Teclado mecânico</span> <span class="preco">289.90</span></li>
    <li class="item" data-id="2"><span class="nome">Mouse sem fio</span> <span class="preco">149.90</span></li>
    <li class="item esgotado" data-id="3"><span class="nome">Cadeira gamer</span> <span class="preco">1199.00</span></li>
  </ul>
  <input id="cupom" type="text" value="PRIMEIRACOMPRA">
  <button id="aplicar">Aplicar cupom</button>
</main>

O seletor é o endereço, não o morador

Pense numa entrega. document é a cidade inteira; #cupom é um endereço único; .item .preco descreve um tipo de endereço que pode aparecer várias vezes. getElementById vai direto ao número exclusivo. querySelector percorre os endereços que combinam com a descrição e entrega o primeiro.

O nome técnico dessa descrição é seletor CSS, mesmo quando ela aparece no JavaScript. E o método não guarda o elemento para sempre: ele consulta o DOM no instante da chamada e devolve uma referência ao que encontrou — ou null, como um entregador dizendo “esse endereço não existe”. Antes de continuar, troque #cupom por #cupom-inexistente no console e observe o null. Reconhecer essa resposta antes de usar .value evita o erro campeão desta lição.

getElementById não leva cerquilha

O nome já diz: ele recebe o id, não um seletor. Escrever #cupom ali é o erro de digitação mais silencioso do DOM, porque não dá exceção nenhuma:

js
const cupom = document.getElementById('cupom');

console.log(cupom.value);
console.log(cupom.tagName);
console.log(document.getElementById('#cupom'));
PRIMEIRACOMPRA INPUT null

A terceira linha procurou por um elemento cujo id é literalmente #cupom. Não existe, então voltou null. Nenhum aviso, nenhuma pista — o bug só aparece duas linhas depois, quando alguém tenta ler .value desse null.

querySelector: o seletor é CSS de verdade

Tudo que funciona na sua folha de estilo funciona aqui — classe, id, atributo, combinação, descendente. E ele devolve o primeiro que casar, na ordem do documento.

js
console.log(document.querySelector('#cupom').value);
console.log(document.querySelector('.item .nome').textContent);
console.log(document.querySelector('.item.esgotado .nome').textContent);
console.log(document.querySelector('[data-id="2"] .preco').textContent);
console.log(document.querySelector('.frete'));
PRIMEIRACOMPRA Teclado mecânico Cadeira gamer 149.90 null

Repare na diferença entre a segunda e a terceira linha. .item .nome — com espaço — quer dizer “um .nome dentro de um .item”, e o primeiro é o teclado. .item.esgotado — sem espaço — é o mesmo elemento com as duas classes, e aí só a cadeira serve. Um espaço muda o resultado inteiro.

A última linha é o comportamento que você mais vai encontrar: seletor válido, elemento inexistente, null de volta.

querySelectorAll devolve uma NodeList estática

Quando você quer todos, e não o primeiro, o método é querySelectorAll. Ele devolve uma NodeList — parecida com array, mas não é array. E ela é uma fotografia: o que estava na página no instante da busca.

js
const itens = document.querySelectorAll('.item');

console.log(itens.length, Object.prototype.toString.call(itens));

document.querySelector('.itens').insertAdjacentHTML(
  'beforeend',
  '<li class="item"><span class="nome">Headset</span></li>',
);

console.log('mesma NodeList:', itens.length);
console.log('nova busca:    ', document.querySelectorAll('.item').length);
3 [object NodeList] mesma NodeList: 3 nova busca: 4

Inserimos um quarto item, mas a variável itens continua com três. Ela guardou o resultado daquele momento e não olha mais para a página. Para ver o novo, você precisa buscar de novo.

Isso é uma vantagem, não uma limitação: enquanto você percorre a lista, ela não muda embaixo dos seus pés.

getElementsByClassName devolve uma coleção viva

Agora o oposto. Os métodos antigos — getElementsByClassName, getElementsByTagName, document.forms — devolvem uma HTMLCollection, que é uma consulta permanente. Ela se atualiza sozinha:

js
const vivos = document.getElementsByClassName('item');

console.log(vivos.length, Object.prototype.toString.call(vivos));

document.querySelector('.itens').insertAdjacentHTML(
  'beforeend',
  '<li class="item"><span class="nome">Headset</span></li>',
);

console.log('depois de inserir:', vivos.length);

vivos[0].remove();
console.log('depois de remover:', vivos.length);
3 [object HTMLCollection] depois de inserir: 4 depois de remover: 3

Ninguém buscou de novo. A variável vivos mudou de tamanho três vezes porque ela não guarda elementos: guarda a pergunta “quem tem a classe item?”, e responde de novo a cada vez que você a consulta.

O laço que come a si mesmo

Essa vivacidade parece esperta até você escrever o laço mais óbvio do mundo para esvaziar o carrinho:

js
const vivos = document.getElementsByClassName('item');

for (let i = 0; i < vivos.length; i++) {
  vivos[i].remove();
}

console.log('sobraram:', document.querySelectorAll('.item').length);
sobraram: 1

Três itens, um laço que parece percorrer os três, e um sobrou. Siga a conta: com i = 0, você remove o primeiro item e a coleção encolhe para 2 — o que era o segundo item virou o índice 0. Com i = 1, você remove o que agora está na posição 1, ou seja, o terceiro item original. A coleção encolhe para 1. Com i = 2, a condição do laço compara 2 com 1, dá falso, e o laço acaba. O segundo item nunca foi tocado.

Três formas de resolver, em ordem de preferência:

js
document.querySelectorAll('.item').forEach((item) => item.remove());

for (const item of [...document.getElementsByClassName('item')]) item.remove();

document.querySelector('.itens').replaceChildren();

A primeira usa a NodeList estática, que não muda enquanto você percorre. A segunda copia a coleção viva para um array antes de mexer. A terceira nem percorre: manda a lista inteira ficar vazia.

forEach: NodeList tem, HTMLCollection não

Nenhuma das duas é array. A diferença é que a NodeList ganhou um forEach próprio na especificação, e a HTMLCollection não:

js
document.querySelectorAll('.item').forEach((item) => {
  console.log(item.querySelector('.nome').textContent);
});

document.getElementsByClassName('item').forEach((item) => {
  console.log(item.textContent);
});
Teclado mecânico Mouse sem fio Cadeira gamer Uncaught TypeError: document.getElementsByClassName(...).forEach is not a function at percorrer.js:5:41

E forEach é só o começo: nem NodeList nem HTMLCollection têm map, filter ou reduce. Para usar o arsenal de array, converta primeiro:

js
const nomes = Array.from(
  document.getElementsByClassName('item'),
  (li) => li.querySelector('.nome').textContent,
);
console.log(nomes.join(' | '));

const precos = [...document.querySelectorAll('.preco')].map((el) => Number(el.textContent));
console.log('total: R$', precos.reduce((soma, p) => soma + p, 0).toFixed(2));
Teclado mecânico | Mouse sem fio | Cadeira gamer total: R$ 1638.80

Array.from aceita uma função de transformação como segundo argumento, o que poupa um .map encadeado. O spread [...] é mais curto quando você só quer o array puro. Os dois funcionam com qualquer uma das duas coleções.

querySelectorAll getElementsByClassName
devolve NodeList HTMLCollection
atualiza sozinha não sim
aceita seletor CSS sim só o nome da classe
tem forEach sim não
acesso por índice sim sim

Os seletores que valem decorar

Como querySelector aceita CSS inteiro, aprender quatro pseudoclasses economiza muito filtro escrito na mão. Todas funcionam nos dois métodos:

js
console.log(document.querySelectorAll('.item:not(.esgotado)').length);
console.log(document.querySelector('.item:last-child .nome').textContent);
console.log(document.querySelectorAll('.item:nth-child(odd) .nome').length);
console.log(document.querySelector('.itens:has(.esgotado)').className);
console.log(document.querySelectorAll('[data-id]').length);
console.log(document.querySelector('[data-id^="1"] .nome').textContent);
2 Cadeira gamer 2 itens 3 Teclado mecânico

Linha por linha: :not() exclui, e é o jeito mais curto de dizer “todos os itens disponíveis”. :last-child pega o último sem precisar de índice. :nth-child(odd) pega as posições ímpares — útil para tratar linhas alternadas de tabela. :has() é a novidade que faltava há vinte anos: ele seleciona o pai a partir do filho, e aqui devolveu a <ul> porque ela contém um item esgotado. E o seletor de atributo aceita comparação parcial: ^= começa com, $= termina com, *= contém.

Escrever .item:not(.esgotado) num seletor é sempre melhor do que buscar tudo e filtrar depois com filter. O trabalho acontece no motor de seleção do navegador, em código nativo, e a intenção fica legível numa linha só.

Nem toda NodeList é estática

Aqui vai um detalhe que quase todo material esquece de contar, e que estraga a regra decorada “NodeList é estática, HTMLCollection é viva”. O que decide não é o tipo: é quem produziu a coleção. A NodeList que sai de querySelectorAll é estática; a que sai de childNodes é viva.

js
const lista = document.querySelector('.itens');

const daBusca = document.querySelectorAll('.item');
const filhos = lista.childNodes;

console.log(Object.prototype.toString.call(daBusca), Object.prototype.toString.call(filhos));
console.log('antes  -> busca:', daBusca.length, '| childNodes:', filhos.length);

lista.append(document.createElement('li'));

console.log('depois -> busca:', daBusca.length, '| childNodes:', filhos.length);
[object NodeList] [object NodeList] antes -> busca: 3 | childNodes: 7 depois -> busca: 3 | childNodes: 8

Os dois objetos são NodeList. Um não se mexeu, o outro cresceu junto com a página. E os sete nós filhos, contra três itens, são os nós de texto da indentação, que você já conhece de o que é o DOM.

A regra que sobrevive à prática é mais curta: só o resultado de querySelectorAll é uma fotografia. Qualquer outra coleção que o DOM te entrega — childNodes, children, getElementsBy*, form.elements — é uma consulta ao vivo. Se você vai modificar a página enquanto percorre, copie para um array antes.

Buscar dentro de um elemento — e a pegadinha do escopo

querySelector e querySelectorAll também existem em qualquer elemento, não só em document. Buscar a partir de um pedaço da página é mais rápido e evita pegar o elemento errado de outra seção:

js
const lista = document.querySelector('.itens');

console.log(lista.querySelectorAll('.nome').length);
console.log(lista.querySelectorAll('.itens .item').length);
console.log(lista.querySelectorAll(':scope > .item').length);
3 3 3

A segunda linha é a surpresa. Você pediu .itens .item a partir da própria .itens — e ela não é descendente de si mesma. Mesmo assim voltaram três. O motivo: o seletor é avaliado contra o documento inteiro, e só depois o resultado é filtrado para o que está dentro do elemento. A <ul> casa como ancestral, os <li> casam como descendentes dela, e todos os três estão dentro da lista. Passa.

Quando você quer mesmo dizer “a partir daqui”, use a pseudoclasse :scope, como na terceira linha. :scope > .item é “filho direto deste elemento”.

closest e matches: subir e testar

O caminho inverso — do elemento clicado para o container dele — é closest. Ele sobe pela árvore até achar um ancestral que case com o seletor, testando o próprio elemento antes:

js
const preco = document.querySelector('[data-id="2"] .preco');

console.log(preco.closest('.item').dataset.id);
console.log(preco.matches('.preco'), preco.matches('.nome'));
console.log(preco.closest('#carrinho').tagName);
console.log(preco.closest('.checkout'));
2 true false MAIN null

closest é o que faz delegação de evento funcionar: você escuta o clique na lista inteira e pergunta evento.target.closest('.item') para descobrir em qual item a pessoa clicou, mesmo que ela tenha acertado o <span> do preço. matches só responde sim ou não para o próprio elemento.

E como todo o resto desta lição, os dois devolvem null quando não acham.

Quando o id vem do banco e não vira seletor

Esta armadilha aparece assim que os ids da sua página passam a vir de um banco de dados. Imagine que o item do carrinho carregue o id do produto:

html
<li class="item" data-id="2" id="7841"><span class="nome">Mouse sem fio</span></li>
js
console.log(document.getElementById('7841').querySelector('.nome').textContent);

try {
  document.querySelector('#7841');
} catch (erro) {
  console.log(erro.name + ':', erro.message);
}

console.log(CSS.escape('7841'));
console.log(document.querySelector('#' + CSS.escape('7841')).dataset.id);
Mouse sem fio SyntaxError: Failed to execute 'querySelector' on 'Document': '#7841' is not a valid selector. \37 841 2

O id 7841 é perfeitamente válido em HTML. Como seletor CSS, não é: um identificador não pode começar com dígito. getElementById não liga, porque compara strings. querySelector precisa interpretar a string como seletor, e aí lança SyntaxError — o único método desta lição que dá exceção em vez de devolver null.

CSS.escape resolve, transformando o dígito inicial numa sequência de escape (aquele \37 841 esquisito da saída, que é o 7 escapado seguido de 841).

Velocidade: a diferença que quase não existe

Vale desarmar um mito antes que ele custe legibilidade ao seu código. Sim, getElementById é mais rápido: ele consulta um índice interno de ids, enquanto querySelector precisa interpretar o seletor e percorrer a árvore. A pergunta é quanto isso pesa. Um milhão de buscas de cada tipo, no Chromium 151:

js
const VOLTAS = 1000000;

function medir(rotulo, busca) {
  const inicio = performance.now();
  for (let i = 0; i < VOLTAS; i++) busca();
  console.log(rotulo, (performance.now() - inicio).toFixed(0) + ' ms');
}

medir('getElementById           :', () => document.getElementById('cupom'));
medir('querySelector #cupom     :', () => document.querySelector('#cupom'));
medir('querySelector .item .nome:', () => document.querySelector('.item .nome'));
getElementById : 12 ms querySelector #cupom : 26 ms querySelector .item .nome: 47 ms

querySelector é mesmo duas a quatro vezes mais lento. Só que a conta que importa é a outra: 26 ms para um milhão de buscas dão 26 nanossegundos por chamada. Uma tela normal faz algumas dezenas de buscas na vida inteira.

Escolha por clareza, não por essa tabela. O único caso em que a diferença sai do ruído é quando a busca está dentro de um laço que roda milhares de vezes — e aí o conserto certo não é trocar de método, é buscar uma vez fora do laço e guardar o resultado numa variável.

O erro campeão: Cannot read properties of null

Este é o erro que você vai ver mais vezes no DevTools nos próximos meses:

js
const cupom = document.getElementById('cupom-desconto');

console.log(cupom.value);
Uncaught TypeError: Cannot read properties of null (reading 'value') at aplicar-cupom.js:3:19

Leia a mensagem com atenção, porque ela é precisa. Cannot read properties of null quer dizer que a coisa antes do ponto é null. (reading 'value') diz qual propriedade você tentou ler. Traduzindo: a busca não achou o elemento.

A linha do erro aponta para o console.log, mas o problema está na linha 1. É sempre assim: o null nasce na busca e explode no primeiro uso. Três causas, em ordem de frequência:

  1. O script rodou antes do HTML. Ponha defer na tag do script — ver o que é o DOM.
  2. O seletor está errado. Id com maiúscula trocada, classe no plural, cerquilha esquecida no querySelector ou cerquilha a mais no getElementById.
  3. O elemento realmente não existe naquele momento. Ele é criado depois, por outro trecho de código, ou só aparece em outra tela.

Quando o alvo é opcional de verdade, teste antes em vez de deixar quebrar:

js
const cupom = document.getElementById('cupom-desconto');

if (!cupom) {
  console.log('esta página não tem campo de cupom');
} else {
  console.log(cupom.value);
}

console.log(document.getElementById('cupom-desconto')?.value ?? 'sem cupom');
esta página não tem campo de cupom sem cupom

O encadeamento opcional (?.) resolve em uma linha, mas use com cuidado: ele esconde o problema tão bem que um seletor digitado errado passa despercebido para sempre. Em elemento que tem que existir, deixe quebrar — o erro é informação.

Como escolher no dia a dia

Três hábitos que economizam tempo depois:

  • Guarde o resultado numa constante em vez de repetir a busca. Além de mais rápido, dá um nome ao elemento: const botaoAplicar = ... diz mais que o seletor solto no meio de uma linha comprida.
  • Marque o que o JavaScript usa. Classe é do CSS; quando o designer renomeia .item para .produto-card, seu script quebra junto. Um atributo próprio, como data-js="aplicar-cupom", deixa explícito que aquele gancho tem código atrás e não deve ser renomeado por engano.
  • Busque a partir do container, não do document, quando estiver dentro de um componente. Fica mais rápido e impede que o mesmo seletor pegue o elemento de outra parte da página.

Com o elemento na mão, o próximo passo é escrever nele: innerHTML e textContent para alterar o HTML é a próxima lição da trilha de JavaScript. O guia completo de JavaScript mostra onde o DOM entra no caminho inteiro.

Prefere aprender em vídeo?

Tem uma aula sobre este assunto no nosso canal.

Ver todos os vídeos do canal
  • dom
  • queryselector
  • getelementbyid
  • nodelist
  • htmlcollection

Perguntas frequentes

querySelector é mais lento que getElementById?
É, de duas a quatro vezes — mas a medição desta lição mostra 26 ms para um milhão de buscas, ou seja, 26 nanossegundos cada. Numa tela real a diferença some no ruído. Escolha por legibilidade.
Quando eu devo usar getElementsByClassName em vez de querySelectorAll?
Quando você quer justamente a coleção viva — um contador que precisa refletir inserções feitas depois, por exemplo. Fora esse caso específico, querySelectorAll é mais previsível e tem forEach.
Por que meu querySelector devolve null se o elemento está lá?
Quase sempre porque o script rodou antes do HTML ser lido. Ponha defer na tag do script. Se não for isso, o seletor está errado — um ponto a menos na classe ou um id com maiúscula trocada.
Posso usar variável dentro do seletor?
Pode, é só montar a string. Mas se o valor vem do usuário, escape com CSS.escape antes: um valor com espaço ou aspas quebra o seletor e derruba a página com SyntaxError.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em Chromium 151.0.7922.34, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. MDN — Document.querySelector() — developer.mozilla.org
  2. MDN — Localizando elementos DOM com seletores — developer.mozilla.org

Continue por aqui