Objeto event, preventDefault e delegação de eventos
O parâmetro event que o navegador entrega sozinho, a diferença entre target e currentTarget, as três fases do evento e delegação com saída real de cada uma.
Todo listener recebe um argumento que você não passou: o objeto do evento. Ele
carrega o que aconteceu (type), onde aconteceu (target), quem está ouvindo
(currentTarget) e os dois métodos que mais confundem — preventDefault, que
cancela a ação padrão do navegador, e stopPropagation, que impede o evento de
subir para os elementos de fora.
Não são sinônimos. preventDefault diz “o link não navega”; stopPropagation
diz “o <div> de fora não fica sabendo do clique”. Trocar um pelo outro é o que
faz um formulário recarregar a página apesar de o seu código estar lá.
A lição anterior mostrou como registrar o listener com addEventListener; esta é sobre o que chega dentro dele.
O formulário de ocorrência que chega com cada evento
Quando toca o alarme de uma loja, o segurança não recebe apenas “algo aconteceu”. A ficha informa qual sensor disparou, em que setor ele está e por quais áreas o aviso passou. O navegador faz algo parecido com cada clique, digitação ou envio de formulário.
Essa ficha é o objeto Event. target aponta para a origem, currentTarget
para quem está executando o listener, e os métodos alteram o comportamento
normal ou a propagação. Na primeira execução, imprima mentalmente esses dois
alvos antes de conferir a saída: se forem diferentes, você já localizou onde o
evento nasceu e onde está sendo tratado.
Você declara o nome; o valor vem de fora.
const carrinho = document.querySelector('#carrinho');
carrinho.addEventListener('click', (evento) => {
console.log('type:', evento.type);
console.log('target:', evento.target.tagName, '|', evento.target.className);
console.log('currentTarget:', evento.currentTarget.tagName, '|', evento.currentTarget.id);
console.log('bubbles:', evento.bubbles, '| cancelable:', evento.cancelable);
});Clicando no botão “Remover” do primeiro item da lista:
Esse evento é o mesmo objeto para todos os listeners daquele disparo. event,
e, evt, evento — o nome é seu; a convenção mais comum em projeto brasileiro
é evento ou e. Como qualquer parâmetro de função,
ele só existe dentro do listener.
target não é currentTarget
Essa distinção é o coração da delegação.
| propriedade | aponta para | muda durante a propagação |
|---|---|---|
event.target |
o elemento mais interno que originou o evento | não, é sempre o mesmo |
event.currentTarget |
o elemento onde este listener está pendurado | sim, um por listener |
No exemplo acima, o clique nasceu no <button class="remover"> — esse é o
target. O listener estava no <ul id="carrinho"> — esse é o currentTarget.
O <button> nem tem listener; ele só serviu de ponto de partida.
As três fases
Um clique não acontece só no elemento clicado. Ele desce da raiz até o alvo
(captura), acontece no alvo (alvo) e sobe de volta (borbulha). Cada
listener escolhe em qual dessas passagens quer entrar: o quarto argumento true
(ou { capture: true }) coloca o listener na descida.
const secao = document.querySelector('#checkout');
const form = document.querySelector('#pagamento');
const botao = document.querySelector('#confirmar');
const fase = { 1: 'captura', 2: 'alvo', 3: 'borbulha' };
secao.addEventListener('click', (e) => console.log('section:', fase[e.eventPhase]), true);
form.addEventListener('click', (e) => console.log('form:', fase[e.eventPhase]), true);
botao.addEventListener('click', (e) => console.log('button:', fase[e.eventPhase]));
form.addEventListener('click', (e) => console.log('form:', fase[e.eventPhase]));
secao.addEventListener('click', (e) => console.log('section:', fase[e.eventPhase]));Um único clique no botão:
Cinco listeners, um clique. A ordem não é a ordem em que você escreveu o código —
é a geometria do HTML. Por padrão (capture ausente ou false), todo listener
fica na borbulha — a fase em que o evento sobe de dentro para fora, do alvo em
direção à raiz. É por isso que um listener pendurado no pai recebe os cliques
que nasceram nos filhos.
preventDefault cancela a ação; stopPropagation cancela a viagem
Ação padrão é o que o navegador faria sozinho: link navega, formulário envia, checkbox marca, botão direito abre o menu de contexto.
const card = document.querySelector('#card-produto');
const link = document.querySelector('#detalhes');
card.addEventListener('click', () => console.log('card: abriu o preview lateral'));
link.addEventListener('click', (evento) => {
evento.preventDefault();
console.log('link: navegação cancelada?', evento.defaultPrevented);
});A navegação foi cancelada e mesmo assim o card recebeu o clique — porque
preventDefault não tem nada a ver com propagação. Trocando para
stopPropagation:
const card = document.querySelector('#card-produto');
const link = document.querySelector('#detalhes');
card.addEventListener('click', () => console.log('card: abriu o preview lateral'));
link.addEventListener('click', (evento) => {
evento.stopPropagation();
console.log('link: parei a subida');
});Agora o card não soube de nada — e o navegador navegou, porque ninguém cancelou a ação padrão. Os dois são independentes e resolvem problemas diferentes:
| método | cancela | não cancela |
|---|---|---|
preventDefault() |
navegar, enviar, marcar, arrastar | os outros listeners |
stopPropagation() |
listeners nos elementos de fora | a ação padrão do navegador |
stopImmediatePropagation() |
tudo acima e os outros listeners do mesmo elemento | a ação padrão |
return false dentro de um listener de addEventListener não faz nenhuma das
duas coisas — isso é herança de onclick inline e não vale aqui.
Delegação: um listener para uma lista que muda
Sem delegação, você registra um listener por item — e os itens criados depois ficam de fora:
const carrinho = document.querySelector('#carrinho');
document.querySelectorAll('.remover').forEach((botao) => {
botao.addEventListener('click', () => console.log('removendo', botao.closest('li').dataset.id));
});
carrinho.insertAdjacentHTML('beforeend',
'<li data-id="8830">Mouse sem fio <button class="remover">Remover</button></li>');Clicando nos dois botões “Remover”:
O segundo botão não respondeu. Ele nasceu depois do forEach, e forEach não
tem como voltar no tempo. Com delegação, o listener mora no <ul>, que já existe
desde o começo:
const carrinho = document.querySelector('#carrinho');
carrinho.addEventListener('click', (evento) => {
const botao = evento.target.closest('.remover');
if (!botao) return;
const item = botao.closest('li');
console.log('removendo item', item.dataset.id);
item.remove();
});
carrinho.insertAdjacentHTML('beforeend',
'<li data-id="8830">Mouse sem fio <button class="remover">Remover</button></li>');Clicando nos dois botões:
Duas linhas fazem o trabalho. evento.target.closest('.remover') sobe a partir
de onde o clique nasceu procurando o botão — resolve o caso do usuário clicar num
<svg> ou num <span> dentro do botão, que é onde o evento.target === botao
falha. E if (!botao) return descarta os cliques em qualquer outro ponto da
lista.
Erros comuns
O formulário recarrega a página
O sintoma é conhecido: você clica em “Aplicar”, o console pisca e some, a página volta ao estado inicial. Não há erro nenhum — o navegador só fez o que sempre fez.
document.querySelector('#aplicar').addEventListener('click', () => {
console.log('validando o cupom FRETE10...');
});O listener rodou, e logo em seguida o navegador enviou o formulário: um
<button> dentro de <form> é type="submit" por padrão. A correção é escutar
o submit do formulário — não o click do botão — e cancelar a ação padrão:
const form = document.querySelector('#cupom');
form.addEventListener('submit', (evento) => {
evento.preventDefault();
console.log('validando o cupom FRETE10...');
console.log('navegação cancelada:', evento.defaultPrevented);
});Chamar o handler sem passar o evento
Quando você embrulha o handler numa arrow que não repassa o argumento, o
parâmetro chega undefined:
function fecharPedido(evento) {
evento.preventDefault();
console.log('pedido fechado');
}
const form = document.querySelector('#pedido');
form.addEventListener('submit', () => fecharPedido());Isolando a chamada — que é exatamente o que a arrow faz:
TypeError: Cannot read properties of undefined (reading ‘preventDefault’) at fecharPedido (file:///private/tmp/loja/checkout-pedido.mjs:5:10) at file:///private/tmp/loja/checkout-pedido.mjs:11:1 at ModuleJob.run (node:internal/modules/esm/module_job:439:25)
Node.js v24.16.0
Passe a função direto — form.addEventListener('submit', fecharPedido) — ou
repasse o argumento: (evento) => fecharPedido(evento). O mesmo vale para
onsubmit="fecharPedido()" escrito no HTML, que também chama sem argumento.
stopPropagation usado como martelo
Colocar stopPropagation em todo listener “para não dar conflito” é o bug mais
caro deste texto, porque ele só aparece semanas depois: o menu que fecha ao
clicar fora para de fechar, o analytics deixa de registrar cliques numa área da
página, o atalho global some. Antes de parar o evento, pergunte se um
if (evento.target.closest('#menu')) return no listener de cima não resolve.
O que levar para o próximo formulário
- Escute o evento certo:
submitno<form>, nãoclickno botão. preventDefaultna primeira linha do handler desubmit, sempre.- Um listener no container com
closest, não um por item. evento.targetpara saber onde o usuário clicou;evento.currentTargetpara saber onde você está ouvindo.stopPropagationsó quando você conseguir explicar qual listener de fora está atrapalhando.
A próxima lição usa tudo isso para
pegar o valor de um input — e mostra por
que value continua sendo string mesmo num campo type="number". O
guia completo de JavaScript tem a trilha inteira em
ordem.
Perguntas frequentes
Preciso declarar o parâmetro event para ele existir?
Quando devo usar capture em vez de bubble?
stopPropagation resolve o problema de dois listeners disputando?
Delegação funciona para todo tipo de evento?
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 Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- MDN — A interface Event — developer.mozilla.org
- MDN — Event: método preventDefault() — developer.mozilla.org
- DOM Standard — Dispatching events — dom.spec.whatwg.org


