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.
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:
<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:
const cupom = document.getElementById('cupom');
console.log(cupom.value);
console.log(cupom.tagName);
console.log(document.getElementById('#cupom'));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.
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'));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.
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);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:
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);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:
const vivos = document.getElementsByClassName('item');
for (let i = 0; i < vivos.length; i++) {
vivos[i].remove();
}
console.log('sobraram:', document.querySelectorAll('.item').length);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:
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:
document.querySelectorAll('.item').forEach((item) => {
console.log(item.querySelector('.nome').textContent);
});
document.getElementsByClassName('item').forEach((item) => {
console.log(item.textContent);
});E forEach é só o começo: nem NodeList nem HTMLCollection têm map,
filter ou reduce. Para usar o arsenal de array, converta primeiro:
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));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:
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);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.
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);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:
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);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:
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'));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:
<li class="item" data-id="2" id="7841"><span class="nome">Mouse sem fio</span></li>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);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:
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'));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:
const cupom = document.getElementById('cupom-desconto');
console.log(cupom.value);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:
- O script rodou antes do HTML. Ponha
deferna tag do script — ver o que é o DOM. - O seletor está errado. Id com maiúscula trocada, classe no plural,
cerquilha esquecida no
querySelectorou cerquilha a mais nogetElementById. - 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:
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');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
.itempara.produto-card, seu script quebra junto. Um atributo próprio, comodata-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.
Perguntas frequentes
querySelector é mais lento que getElementById?
Quando eu devo usar getElementsByClassName em vez de querySelectorAll?
Por que meu querySelector devolve null se o elemento está lá?
Posso usar variável dentro do seletor?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
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
- MDN — Document.querySelector() — developer.mozilla.org
- MDN — Localizando elementos DOM com seletores — developer.mozilla.org



