JavaScript no cliente e DOM
Tipos, arrays, funções, closures, objetos, protótipos, DOM, eventos e delegação.
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:
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);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:
function criarContador(inicial) {
let valor = inicial;
return () => ++valor;
}
const a = criarContador(5);
const b = criarContador(0);
console.log(a(), a(), b(), a());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:
| Chamada | Devolve | a | b |
|---|---|---|---|
| Criar os contadores | Nenhum | 5 | 0 |
Primeiro a() | 6 | 6 | 0 |
Segundo a() | 7 | 7 | 0 |
b() | 1 | 7 | 1 |
Terceiro a() | 8 | 8 | 1 |
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.
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ção | Nó em origem | Em destino | Listeners do item |
|---|---|---|---|
destino.append(item) | Retirado | O mesmo objeto | Conservados |
destino.append(item.cloneNode(true)) | Permanece | Outro objeto, descendentes copiados | A 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.
CSS e JavaScript
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.
CSS e JavaScript
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.
function totalAte(precos, limite) {
return 0;
}Verifica o código para comparar o resultado com os testes.
Testes
Selecionar sem alterar o array
Código do testeconst precos = [18, 12, 32]; console.log(totalAte(precos, 20)); console.log(JSON.stringify(precos));Esperado:30 [18,12,32]
Array vazio
Código do testeconsole.log(totalAte([], 20));Esperado:0
Incluir o limite
Código do testeconsole.log(totalAte([20, 21], 20));Esperado:20
Nenhum preço elegível
Código do testeconsole.log(totalAte([25, 30], 20));Esperado:0
Repetidos e limite zero
Código do testeconsole.log(totalAte([12, 12, 25], 20)); console.log(totalAte([0, 1], 0));Esperado:24 0
const precos = [18, 12, 32];
console.log(totalAte(precos, 20));
console.log(JSON.stringify(precos));30 [18,12,32]
console.log(totalAte([], 20));0
console.log(totalAte([20, 21], 20));20
console.log(totalAte([25, 30], 20));0
console.log(totalAte([12, 12, 25], 20));
console.log(totalAte([0, 1], 0));24 0
Pista
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.
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.
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.
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
thisindefinido 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.
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.
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.
function ordenarPrecos(precos) {
return [];
}Verifica o código para comparar o resultado com os testes.
Testes
Ordem numérica e entrada preservada
Código do testeconst precos = [8, 25, 3]; console.log(JSON.stringify(ordenarPrecos(precos))); console.log(JSON.stringify(precos));Esperado:[3,8,25] [8,25,3]
Lista vazia
Código do testeconsole.log(JSON.stringify(ordenarPrecos([])));Esperado:[]
Repetidos e zero
Código do testeconsole.log(JSON.stringify(ordenarPrecos([12, 0, 12, 2])));Esperado:[0,2,12,12]
Cópia independente
Código do testeconst 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] []
const precos = [8, 25, 3];
console.log(JSON.stringify(ordenarPrecos(precos)));
console.log(JSON.stringify(precos));[3,8,25] [8,25,3]
console.log(JSON.stringify(ordenarPrecos([])));[]
console.log(JSON.stringify(ordenarPrecos([12, 0, 12, 2])));[0,2,12,12]
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));[4] []
Pista
sort altera o array e, sem comparador, compara texto.Mais uma pista
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
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.
