JavaScript no cliente e DOM

Tipos, arrays, funções, closures, objetos, protótipos, DOM, eventos e delegação.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

Ao acrescentar uma tarefa de leitura, o JavaScript cria um item na página já carregada. O array e a função que calculam valores pertencem à linguagem; document e os elementos pertencem à API DOM do navegador. Por isso o cálculo de preços pode correr sem uma página, mas criar um li exige um documento.

Tipos e conversões

Usa const para uma ligação que não será reatribuída e let quando será. const não torna um objeto imutável. var tem âmbito de função e regras de declaração diferentes do âmbito de bloco de let e const.

const livro = { titulo: 'Redes' };
livro.titulo = 'SQL'; // permitido
// livro = {};       // reatribuição proibida

Os tipos primitivos incluem string, number, boolean, null, undefined, bigint e symbol. typeof null devolve historicamente 'object', apesar de null não ser um objeto. undefined aparece, por exemplo, ao ler uma propriedade ausente.

console.log('8' + 1); // 81, string
console.log('8' - 1); // 7, number
console.log('8' === 8); // false
console.log(Number('8') + 1); // 9
console.log(Number('8x')); // NaN

+ pode concatenar; - exige operandos numéricos. === evita conversões implícitas na igualdade. NaN significa que o resultado não é um número válido; usa Number.isNaN para o detetar e Number.isFinite quando precisas de excluir também infinitos. Number('') é 0, por isso confirma uma entrada obrigatória antes de converter.

parseInt('8x', 10) devolve 8, porque lê um prefixo numérico; Number('8x') dá NaN. Para exigir um inteiro completo, verifica primeiro a entrada vazia, converte e usa Number.isInteger, além dos limites pretendidos. Duas criações de objetos, como [1] === [1], comparam referências e dão false, mesmo com valores iguais.

Num teste booleano, 0, '', null, undefined, NaN e false são falsos; arrays e objetos vazios são verdadeiros. x || alternativa substitui todos os valores falsos; x ?? alternativa só substitui null e undefined. Uma quantidade válida igual a zero pode exigir ??, não ||.

Arrays e funções como valores

for...of percorre valores de um iterável; for...in percorre nomes de propriedades e não é o percurso habitual para valores de um array. map transforma, filter seleciona e reduce acumula:

Filtrar e somar preços
const livros = [
  { titulo: 'Redes', preco: 18 },
  { titulo: 'Algoritmos', preco: 25 },
  { titulo: 'SQL', preco: 12 },
];
const baratos = livros.filter((livro) => livro.preco < 20);
console.log(baratos.map((livro) => livro.titulo).join(', '));
console.log(baratos.reduce((total, livro) => total + livro.preco, 0));
console.log(livros.length);
Resultado

Executa o código para ver a saída e os gráficos.

Segue os estados intermédios. À entrada, três livros com preços 18, 25 e 12. O filter com limite 20 conserva dois livros, Redes e SQL. O map para títulos produz Redes, SQL. O reduce com total inicial 0 soma 18 + 12 = 30. O array original conserva três entradas, por isso o programa obtém Redes, SQL, 30 e 3.

Os objetos dentro de uma cópia superficial continuam partilhados. Alterar baratos[0].preco também altera esse objeto em livros, porque ambos os arrays referem o mesmo objeto.

Uma função é um valor que pode ser passado como callback. livros.map((livro) => livro.titulo) recebe uma função; escrever map(obterTitulo()) chamaria obterTitulo imediatamente e passaria o resultado. Numa arrow function com corpo entre chavetas, usa return para devolver um valor.

A desestruturação extrai partes, como const { titulo, preco } = livro. Spread, como [...livros], cria uma cópia superficial. Um Set guarda valores únicos; um Map associa chaves a valores sem limitar as chaves a nomes de propriedades de um objeto.

slice cria uma cópia de uma parte; splice altera o array ao remover ou inserir. push e pop alteram o fim. sort também altera o array e, sem comparador, compara representações textuais:

const precos = [8, 25, 3];
console.log([...precos].sort()); // [25, 3, 8]
console.log([...precos].sort((a, b) => a - b)); // [3, 8, 25]
console.log(precos); // [8, 25, 3]

O comparador devolve um número negativo se a vem antes de b, zero se empatam e positivo se vem depois. A cópia com spread protege a ordem original. forEach percorre para produzir efeitos e devolve undefined; usa map quando precisas de construir um array com os resultados.

Âmbito e closures

O âmbito lexical depende do local onde a função foi criada. Uma closure é uma função com acesso ao ambiente lexical que conserva:

Conservar contadores numa closure
function criarContador(inicial) {
  let valor = inicial;
  return () => ++valor;
}
const a = criarContador(5);
const b = criarContador(0);
console.log(a(), a(), b(), a());
Resultado

Executa o código para ver a saída e os gráficos.

Cada chamada a criarContador cria uma ligação de valor. A função devolvida conserva acesso a essa ligação. Na tabela, as colunas a e b mostram o valor guardado por cada contador:

ChamadaDevolveab
Criar os contadoresNenhum50
Primeiro a()660
Segundo a()770
b()171
Terceiro a()881

A saída é 6 7 1 8. O último incremento pertence a a; chamar b não altera o estado de a.

Com let, cada iteração de um for pode ter uma ligação própria, útil em callbacks. Com um var partilhado, callbacks que correm depois do ciclo podem ler todos o valor final. Prevê o momento de execução, não apenas o lugar onde o callback foi escrito.

O vídeo apresenta a linguagem. Nos exemplos seguintes, distingue sempre o local de criação de uma função do modo como ela é chamada.

Abrir no YouTube

Objetos, this e protótipos

Num método normal chamado como objeto.metodo(), this refere habitualmente esse objeto. Separar o método do objeto pode perder essa associação:

'use strict';
const livro = {
  titulo: 'Redes',
  nome() {
    return this.titulo;
  },
};
const nome = livro.nome;
console.log(livro.nome()); // Redes
console.log(nome.call(livro)); // Redes
const nomeLigado = nome.bind(livro);
console.log(nomeLigado()); // Redes

Chamar apenas nome() neste exemplo estrito tenta aceder a titulo num this indefinido, e o programa falha. bind cria uma função ligada ao recetor; call faz uma chamada única com um this explícito. São operações diferentes: uma produz uma função para usar depois, a outra executa já.

Uma arrow function herda this do ambiente em que foi criada, não o recebe do objeto pela mesma regra. Por isso não a uses como substituto automático de um método que depende de this.

Se uma propriedade não existe no objeto, a procura pode continuar no seu protótipo e depois nos protótipos seguintes. Classes usam esse mecanismo:

class Livro {
  constructor(titulo) {
    this.titulo = titulo;
  }
  nome() {
    return this.titulo;
  }
}
const redes = new Livro('Redes');
console.log(redes.nome()); // Redes
console.log(redes.hasOwnProperty('titulo')); // true
console.log(redes.hasOwnProperty('nome')); // false

Procura a propriedade por etapas. titulo pertence à instância, por isso encontra-se logo no objeto. nome não está na instância e a procura continua em Livro.prototype, onde encontra o método. hasOwnProperty distingue os dois casos. extends liga cadeias de protótipos, super permite chamar a implementação da classe base e membros static pertencem à classe. Campos privados com # têm uma restrição de acesso própria da linguagem.

Ler e alterar o DOM

querySelector devolve o primeiro elemento ou null; querySelectorAll devolve uma NodeList estática. children contém elementos; childNodes inclui também texto e comentários. Por isso, espaços e mudanças de linha podem alterar childNodes sem acrescentar elementos.

Para percorrer elementos, usa parentElement, firstElementChild e nextElementSibling. closest(seletor) começa no próprio elemento e procura um antepassado correspondente. classList altera classes; dataset.id lê data-id. Uma propriedade como input.value representa o valor atual, que pode diferir do atributo inicial escrito no HTML.

children é uma coleção viva: acompanha inserções e remoções. Uma NodeList de querySelectorAll conserva a seleção feita naquele momento. Para usar map numa NodeList, cria primeiro um array com Array.from(nos) ou [...nos].

const item = document.createElement('li');
item.textContent = '<b>Redes</b>';
lista.append(item);

O utilizador vê o texto literal <b>Redes</b>. innerHTML interpretaria marcação. Para dados do utilizador, textContent evita misturar dados com HTML.

Inserir um nó existente move-o para o novo pai, sem o copiar:

OperaçãoNó em origemEm destinoListeners do item
destino.append(item)RetiradoO mesmo objetoConservados
destino.append(item.cloneNode(true))PermaneceOutro objeto, descendentes copiadosA cópia não recebe os de addEventListener

Ao clonar, evita duplicar IDs. Os listeners dos antepassados não acompanham o nó: os eventos futuros propagam-se pela nova árvore.

append acrescenta nós; replaceChildren substitui todos os filhos; remove retira o nó. Mudar style aplica declarações em linha; mudar uma classe permite manter o aspeto no CSS.

Eventos e delegação

Considera ul > li > button > span. Ao clicar no span, ele é o target. O evento chega ao listener do ul durante a subida; nesse listener, currentTarget é o ul. O alvo conserva-se, mas o elemento do listener muda ao longo do percurso.

A figura omite os antepassados exteriores ao ul. Na captura, o percurso aproxima-se do alvo; no bubbling, sobe pelos antepassados, quando o tipo de evento o permite.

closest não faz parte da propagação. É a procura que o programa faz a partir do alvo para encontrar o botão e o item: começa no span e sobe até ao seletor pedido.

preventDefault() impede uma ação por defeito, como submeter o formulário. Não pára a propagação. stopPropagation() interrompe a propagação, não a ação por defeito. Estas operações resolvem problemas diferentes.

HTML / CSS / JavaScript
Código a editar
HTML
CSS e JavaScript
CSS
JavaScript
Pré-visualização

A preparar a pré-visualização…

Consola
Sem mensagens.

A lista começa com duas tarefas. Clica no texto Remover de uma delas e observa o contador passar de 2 para 1. Escreve Rever HTTP, adiciona e remove essa nova tarefa. O formulário cria um li com o texto e um botão cujo rótulo é um span. O listener de remoção fica no ul, não em cada botão: o clique sobe, closest identifica o botão e o programa remove o li correspondente. A delegação funciona tanto nos itens iniciais como nos criados depois de registares o listener.

O contador usa children.length, porque conta itens e não nós de texto. O texto entra por textContent. Experimenta escrever <img src=x> e confirma que aparece literalmente, sem criar uma imagem. O estado visível muda apenas no cliente; recarregar descarta esta lista.

Carregamento e temporizadores

Um script clássico com defer executa depois da análise do documento e antes de DOMContentLoaded, conservando a ordem entre scripts deferidos. async executa quando o download termina, sem garantir essa ordem nem esperar pela análise completa. Módulos são deferidos por defeito.

setTimeout(callback, atraso) agenda um callback para não correr antes do atraso, mas não garante um instante exato. setInterval repete; usa clearTimeout ou clearInterval para cancelar. O JavaScript corrente termina antes dos callbacks agendados. A ordem entre promessas e temporizadores continua em HTTP e Ajax.

Um <template> guarda conteúdo inerte em template.content, que podes clonar com cloneNode(true) e inserir depois de preencher dados com destinos seguros. Shadow DOM cria uma árvore encapsulada para um componente, separando grande parte dos estilos e estrutura do documento exterior. Não é uma fronteira de segurança para guardar segredos.

Guardar a função que foi registada

bind cria uma função nova com um recetor fixo. Isso resolve o this de um método passado como listener, mas duas chamadas a bind não devolvem o mesmo objeto função:

const callback = leitor.contar.bind(leitor);
botao.addEventListener('click', callback);
// Outra função, mesmo com o mesmo método e recetor:
botao.removeEventListener('click', leitor.contar.bind(leitor));

A remoção acima não encontra a função registada. Guarda o callback e passa essa mesma referência:

botao.removeEventListener('click', callback);

O tipo de evento e a opção de captura também precisam de coincidir. Um listener registado com captura true não é o mesmo registo que um com captura false. Não confundas esta opção com impedir propagação: a captura escolhe a fase em que o listener corre, enquanto stopPropagation interrompe o percurso. Uma arrow criada novamente também seria outra função, mesmo tendo o mesmo corpo.

HTML / CSS / JavaScript
Código a editar
HTML
CSS e JavaScript
CSS
JavaScript
Pré-visualização

A preparar a pré-visualização…

Consola
Sem mensagens.

Carrega em Contar leitura: a contagem passa a 1. Pede a remoção com outro bind e conta de novo: passa a 2, porque o listener continua presente. Depois remove o callback guardado e tenta contar: fica em 2. O método continua a pertencer ao objeto leitor; foi o registo do listener que deixou de chamar a função.

Esta identidade também permite evitar duplicados: voltar a adicionar a mesma combinação de evento, callback e captura não cria outra chamada para o mesmo evento. Criar outro bind e registá-lo acrescentaria um segundo callback distinto. Decide quando criar a função e conserva-a durante o período em que precisas de a remover.

Exercícios

Filtrar e somar preços

Define totalAte(precos, limite): devolve a soma dos preços menores ou iguais ao limite, sem alterar o array nem imprimir. São inteiros não negativos em euros; a soma vazia é zero. Não uses o DOM.

JavaScript
function totalAte(precos, limite) {
  return 0;
}
Testes e resultado

Verifica o código para comparar o resultado com os testes.

Testes
  1. Selecionar sem alterar o array
    Código do teste
    const precos = [18, 12, 32];
    console.log(totalAte(precos, 20));
    console.log(JSON.stringify(precos));
    Esperado:
    30
    [18,12,32]
  2. Array vazio
    Código do teste
    console.log(totalAte([], 20));
    Esperado:
    0
  3. Incluir o limite
    Código do teste
    console.log(totalAte([20, 21], 20));
    Esperado:
    20
  4. Nenhum preço elegível
    Código do teste
    console.log(totalAte([25, 30], 20));
    Esperado:
    0
  5. Repetidos e limite zero
    Código do teste
    console.log(totalAte([12, 12, 25], 20));
    console.log(totalAte([0, 1], 0));
    Esperado:
    24
    0

Pista
Seleciona os preços com filter e acumula com reduce.
Mais uma pista

Inclui o limite com <= e dá a reduce um total inicial igual a zero.

Solução
function totalAte(precos, limite) {
  return precos
    .filter((preco) => preco <= limite)
    .reduce((total, preco) => total + preco, 0);
}

Para [18, 12, 32] e limite 20, entram 18 e 12, cuja soma é 30. Um ciclo for…of com um acumulador também resolve.

Erros frequentes

Excluir o preço igual ao limite, esquecer o valor inicial de reduce, alterar o array recebido ou imprimir em vez de devolver.

Voltar à explicação
Seguir uma cópia superficial

Prevê os três valores escritos e explica a diferença entre reatribuir e alterar o objeto.

const a = [{ n: 2 }, { n: 5 }];
const b = a.filter((x) => x.n < 4);
b[0].n = 9;
console.log(a[0].n, a.length, b.length);

Pista

filter cria outro array, mas não copia profundamente os objetos.

Solução

A saída é 9 2 1. filter cria um array novo com uma entrada, mas o objeto dentro dele é o mesmo objeto de a. Ao escrever b[0].n = 9, alteras esse objeto partilhado, por isso a[0].n passa a 9. Os arrays continuam distintos, com comprimentos 2 e 1. const impede reatribuir a ou b, não alterar objetos referidos.

Confere a tua resposta:

  • Obtive 9, 2 e 1.
  • Expliquei arrays diferentes e objeto partilhado.

Erros frequentes

Não prever 2 para a[0].n nem tratar const como congelamento profundo.

Voltar à explicação
Prever ambientes separados

Prevê a saída e mostra o estado de n de cada closure após cada chamada.

function contador(inicio) {
  let n = inicio;
  return () => (n += 2);
}
const a = contador(1);
const b = contador(10);
console.log(a(), b(), a(), b());

Pista

Cada chamada a contador cria uma ligação de n diferente.

Solução

A saída é 3 12 5 14. Cada chamada a contador cria um n independente. As chamadas correm da esquerda para a direita. Após a(): n de a passa de 1 para 3. Após b(): n de b passa de 10 para 12. O segundo a() leva a de 3 para 5. O segundo b() leva b de 12 para 14. As closures conservam o seu ambiente, mas não partilham n.

Confere a tua resposta:

  • Obtive 3 12 5 14.
  • Segui dois estados independentes.

Erros frequentes

Não usar um único contador global para as duas closures.

Voltar à explicação
Manter o recetor do método

Em modo estrito, um objeto tem titulo: Redes e método normal nome() que devolve this.titulo. Guarda-se const f = livro.nome. Compara livro.nome(), f() e f.bind(livro)(). Onde fica um método definido por class Livro?

Pista

Uma função normal não fica ligada ao objeto só por ser lida numa propriedade.

Mais uma pista

bind fixa o recetor; métodos de classe são habitualmente partilhados no protótipo.

Solução

livro.nome() devolve Redes. f() tem

this

indefinido neste exemplo e falha ao ler titulo. f.bind(livro)() devolve Redes. Um método normal definido na classe fica em Livro.prototype; uma propriedade criada no constructor fica na instância.

Confere a tua resposta:

  • Expliquei a falha de f().
  • Distingui protótipo e propriedade própria.

Erros frequentes

Não substituir indiscriminadamente o método por arrow function, porque ela herda outro this.

Voltar à explicação
Remover itens criados depois

Descreve um listener único num ul que remove o li quando o utilizador clica no botão Remover, incluindo quando clica num span dentro do botão. Distingue target de currentTarget e as funções preventDefault/stopPropagation.

Pista

Procura o botão a partir do alvo com closest.

Mais uma pista

Depois procura o li; o listener está no ul e vê o clique que subiu.

Solução

O listener recebe o evento no ul. Usa evento.target.closest com um seletor dos botões de remoção, confirma que o botão pertence à lista e remove o item com botao.closest('li').remove(). O alvo pode ser o span; currentTarget é o ul. preventDefault impediria uma ação nativa, como submissão, enquanto stopPropagation impediria continuar a subida. A delegação funciona nos novos itens sem lhes acrescentar listeners.

Confere a tua resposta:

  • Usei closest e verificação de pertença.
  • Expliquei alvo, listener e as duas operações.

Erros frequentes

Não testar target como se fosse sempre o botão, nem confundir cancelamento com propagação.

Voltar à explicação
Ordenar preços sem perder a lista original

Define ordenarPrecos(precos): recebe um array de inteiros não negativos e devolve outro array com os mesmos preços por ordem crescente, conservando repetições e a entrada. Mesmo para a lista vazia ou com um preço, devolve um array independente. Não imprimas dentro da função.

JavaScript
function ordenarPrecos(precos) {
  return [];
}
Testes e resultado

Verifica o código para comparar o resultado com os testes.

Testes
  1. Ordem numérica e entrada preservada
    Código do teste
    const precos = [8, 25, 3];
    console.log(JSON.stringify(ordenarPrecos(precos)));
    console.log(JSON.stringify(precos));
    Esperado:
    [3,8,25]
    [8,25,3]
  2. Lista vazia
    Código do teste
    console.log(JSON.stringify(ordenarPrecos([])));
    Esperado:
    []
  3. Repetidos e zero
    Código do teste
    console.log(JSON.stringify(ordenarPrecos([12, 0, 12, 2])));
    Esperado:
    [0,2,12,12]
  4. Cópia independente
    Código do teste
    const precos = [4];
    const copia = ordenarPrecos(precos);
    copia.push(9);
    console.log(JSON.stringify(precos));
    const vazio = [];
    ordenarPrecos(vazio).push(9);
    console.log(JSON.stringify(vazio));
    Esperado:
    [4]
    []

Pista
sort altera o array e, sem comparador, compara texto.
Mais uma pista
Cria uma cópia com spread ou slice antes de ordenar com um comparador numérico.
Solução
function ordenarPrecos(precos) {
  return [...precos].sort((a, b) => a - b);
}

A cópia recebe a ordenação; precos conserva a ordem original. precos.slice().sort((a, b) => a - b) também resolve. Um algoritmo que insira os valores numa nova lista ordenada é igualmente válido.

Erros frequentes

Não ordenar a entrada diretamente, devolver a própria entrada quando tem poucos valores, remover repetições com Set ou comparar os números como strings.
Voltar à explicação
Remover o mesmo registo de evento

botao é um botão existente. O método aumenta o total do objeto, e o callback fica ligado a esse objeto. Segue os registos e os três cliques nos pontos indicados:

const leitor = {
  total: 0,
  contar() {
    ++this.total;
  },
};
const callback = leitor.contar.bind(leitor);
botao.addEventListener('click', callback, true);
botao.addEventListener('click', callback, true);
botao.removeEventListener('click', callback);
// Clique 1.
botao.removeEventListener('click', leitor.contar.bind(leitor), true);
// Clique 2.
botao.removeEventListener('click', callback, true);
// Clique 3.

Qual é o total depois de cada clique? O primeiro clique chama o método duas vezes? Justifica sem confundir o objeto leitor com o registo do listener.

Solução

Os totais são 1, 2 e 2. A segunda adição repete a mesma combinação de tipo, função e captura; não cria outro registo. A primeira remoção usa captura false por omissão, mas o registo foi criado com true, por isso permanece.

A remoção anterior ao segundo clique passa uma função nova devolvida por bind. O método e o recetor são os mesmos, mas a identidade da função difere: o registo permanece e o total chega a 2.

A última remoção fornece o callback original e captura true; o terceiro clique já não chama o método. O objeto e a função continuam a existir, mas já não há aquele listener registado no botão.

Confere a tua resposta:

  • Obtive totais 1, 2 e 2 nos três cliques.
  • Expliquei função nova e captura diferente.
  • Usei a mesma referência e a captura true para remover.
Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.