Desenvolvimento web e mobile
Fundamentos de JavaScript: tipos, escopo, closures, assincronia e DOM
Iniciante9 min de leituraAtualizado
Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês
JavaScript recompensa um bom modelo mental muito mais do que sintaxe decorada: como os valores são convertidos, como o escopo e as closures capturam variáveis, o que decide o valor de this e em que ordem o event loop executa seus callbacks. Este guia constrói esses modelos e depois os aplica a módulos, classes, promises, tratamento de erros, DOM e fetch, com a sintaxe moderna em que você pode confiar hoje.
O que você vai aprender
- Diferenciar primitivos de objetos e prever como a comparação e a coerção se comportam
- Usar let, const, closures e módulos para controlar o escopo e evitar bugs de estado compartilhado
- Explicar o que define o valor de this e quando uma arrow function é a escolha certa
- Transformar dados com métodos de arrays e objetos em vez de loops manuais e mutação
- Raciocinar sobre o event loop e escrever código assíncrono com promises e async/await
- Manipular o DOM e chamar APIs HTTP com fetch, tratando erros de forma deliberada
JavaScript é uma linguagem pequena envolta numa superfície enorme. Quase toda a confusão que as pessoas carregam sobre ela (por que uma comparação retornou algo inesperado, por que uma variável dentro de um callback estava com o valor errado, por que o this sumiu) vem de quatro ou cinco regras de fundo, e não da sintaxe em si. Aprenda essas regras e o resto da linguagem, incluindo o framework que você estiver usando, fica muito mais fácil de entender.
Este guia cobre esse núcleo. Ele parte do princípio de que você já sabe ler um loop for e se concentra no modelo: como os valores se comportam, como o escopo e as closures os capturam, o que determina o this, como o event loop agenda o seu trabalho e como a biblioteca padrão espera que você transforme dados. Ele usa sintaxe moderna do começo ao fim, incluindo recursos finalizados nas edições recentes do ECMAScript, e termina com o DOM e as APIs de rede, onde a maior parte do código de navegador passa o tempo.
Valores: sete primitivos e todo o resto
Existem exatamente sete tipos primitivos (string, number, boolean, undefined, null, bigint e symbol) e todo o resto é objeto, incluindo arrays, funções, datas e expressões regulares. Primitivos são copiados por valor e são imutáveis; objetos são manipulados por referência, então duas variáveis podem apontar para a mesma coisa.
let a = { count: 1 };
let b = a;
b.count = 2;
console.log(a.count); // 2 — same object
let x = "hi";
let y = x;
y += "!";
console.log(x); // "hi" — strings are immutable
number é um float de 64 bits, e é por isso que 0.1 + 0.2 não dá 0.3 e que inteiros acima de 2^53 perdem precisão; use bigint (um literal terminado em n) para inteiros grandes e para aritmética inteira com valores monetários em centavos. typeof null retorna "object", um bug preservado desde 1995 por compatibilidade.
A coerção é onde os iniciantes se queimam. Os operadores + e ==, além do teste de veracidade, convertem tipos implicitamente, e as regras são complicadas. Dois hábitos eliminam quase todo o risco. Primeiro, compare sempre com ===; a única exceção que vale manter é value == null, um teste compacto para "null ou undefined". Segundo, decore exatamente a lista de valores falsy (false, 0, -0, 0n, "", null, undefined, NaN), porque todo o resto, incluindo [], {} e "0", é truthy. Quando quiser um valor padrão só para null e undefined, use o operador de coalescência nula ?? em vez de ||, que também substituiria um 0 ou uma string vazia perfeitamente legítimos. A mesma distinção vale para o encadeamento opcional: user?.address?.city entra em curto-circuito e retorna undefined em vez de lançar uma exceção.
Bindings, escopo e closures
let e const têm escopo de bloco e vivem numa "temporal dead zone" do início do bloco até a execução da declaração, então acessá-los antes da hora lança um ReferenceError em vez de retornar undefined silenciosamente. var tem escopo de função e sofre hoisting, o que é a origem de uma longa lista de bugs clássicos. Use const por padrão; ele impede a reatribuição do binding, mas não congela o objeto para o qual aponta.
Uma closure é uma função junto com o escopo em que foi criada. Como a função interna mantém uma referência viva para as variáveis externas (e não uma cópia), as closures dão ao JavaScript estado privado:
function createCounter(start = 0) {
let value = start; // captured, not copied
return {
increment: () => ++value,
read: () => value,
};
}
const counter = createCounter(10);
counter.increment();
console.log(counter.read()); // 11
Nada de fora consegue alcançar value. Esse padrão sustenta o estado de módulos, a memoização, os event handlers que se lembram do seu contexto e toda função "factory" que você vai encontrar. A pegadinha histórica (um loop que criava handlers que compartilhavam todos o mesmo var i) desaparece com let, que cria um binding novo a cada iteração.
Funções, e o que decide o this
Funções são valores: você pode passá-las, retorná-las e armazená-las. O this de uma função comum não é determinado pelo lugar onde ela foi escrita, mas por como ela é chamada. Chamada como obj.method(), o this é obj. Chamada sozinha, o this é undefined no modo estrito, que é sempre o caso do código de módulos. Chamada com call, apply ou bind, é o que você tiver fornecido. Chamada com new, é o objeto recém-criado.
Arrow functions não têm this próprio; elas capturam, de forma permanente, o do escopo ao redor. É exatamente o que você quer em callbacks e exatamente o que você não quer em métodos de objeto que precisam de um receptor.
const timer = {
seconds: 0,
startBroken() {
setInterval(function () { this.seconds++; }, 1000); // this is not timer
},
startWorking() {
setInterval(() => { this.seconds++; }, 1000); // arrow keeps timer
},
};
Parâmetros padrão, parâmetros rest (...args) e a sintaxe de spread cobrem quase tudo para o que se usava o antigo objeto arguments e, ao contrário dele, funcionam em arrow functions e são arrays de verdade.
Objetos, desestruturação e atualizações imutáveis
Literais de objeto aceitam propriedades abreviadas, chaves computadas e métodos. A desestruturação extrai valores, com valores padrão e renomeação, e o spread copia:
const settings = { theme: "dark", locale: "en", retries: 3 };
const { theme, retries = 1, missing = "fallback" } = settings;
const [first, ...rest] = ["a", "b", "c"];
const updated = { ...settings, theme: "light" }; // new object, old untouched
function connect({ host, port = 5432, ssl = true } = {}) { /* ... */ }
O spread faz uma cópia rasa: objetos aninhados continuam compartilhados. Para um clone profundo de dados simples, structuredClone(value) agora é nativo e lida com ciclos, Maps, Sets e typed arrays, o que JSON.parse(JSON.stringify(x)) não faz. Preferir criar objetos novos em vez de mutar os existentes é o que faz a detecção de mudanças funcionar nos frameworks modernos e o que, de modo geral, torna o estado fácil de entender.
Dois tipos de coleção merecem ser mais usados do que são. Map aceita chaves de qualquer tipo, preserva a ordem de inserção e informa seu tamanho diretamente, o que o torna melhor que um objeto comum para um dicionário com chaves dinâmicas. Set oferece testes de pertinência baratos e deduplicação: [...new Set(items)] é a forma idiomática de obter valores únicos.
Arrays e os métodos que substituem loops
Os métodos de array expressam a intenção, se encadeiam de forma limpa e evitam erros de off-by-one.
| Método | Retorna | Uso típico |
|---|---|---|
map | novo array, mesmo tamanho | Transformar cada elemento |
filter | novo array, menor | Manter os elementos que atendem ao critério |
reduce | um único valor | Reduzir a um total, um objeto ou um agrupamento |
find / findIndex | elemento ou índice | Primeira ocorrência |
some / every | booleano | Testes de existência e universais |
flatMap | novo array achatado | Mapear e depois achatar um nível |
sort | o mesmo array, mutado | Ordenação: passe um comparador |
toSorted / toReversed / with | novo array | Equivalentes sem mutação |
const orders = [
{ id: 1, region: "north", total: 120 },
{ id: 2, region: "south", total: 80 },
{ id: 3, region: "north", total: 45 },
];
const northTotal = orders
.filter((o) => o.region === "north")
.reduce((sum, o) => sum + o.total, 0); // 165
const byRegion = Object.groupBy(orders, (o) => o.region);
const highestFirst = orders.toSorted((a, b) => b.total - a.total);
sort muta o array e, sem comparador, compara os elementos como strings: é por isso que [10, 9, 1].sort() resulta em [1, 10, 9]. Os métodos mais novos toSorted, toReversed, toSpliced e with retornam cópias, o que normalmente é o que você queria. Object.groupBy e Map.groupBy eliminam o reduce escrito à mão mais comum.
Módulos e classes
Os módulos ES são a unidade de código padrão. Cada arquivo tem seu próprio escopo, os imports são resolvidos estaticamente e tudo roda em modo estrito:
// money.js
export const CURRENCY = "EUR";
export function format(cents) {
return new Intl.NumberFormat("fr-FR", { style: "currency", currency: CURRENCY })
.format(cents / 100);
}
export default class Money { /* ... */ }
// checkout.js
import Money, { format, CURRENCY } from "./money.js";
const lazy = await import("./heavy-chart.js"); // dynamic, code-split
Prefira exports nomeados: eles são fáceis de achar com grep, de refatorar e oferecem autocompletar melhor que um export default, cujo nome local cada módulo que o importa inventa de novo. O import() dinâmico retorna uma promise e é o mecanismo por trás das rotas com lazy loading.
Classes são sintaxe sobre a cadeia de protótipos, com campos privados de verdade, prefixados com # e inacessíveis de fora:
class Account {
#balance = 0;
static from(cents) { return new Account(cents); }
constructor(cents = 0) { this.#balance = cents; }
get balance() { return this.#balance; }
deposit(cents) {
if (cents <= 0) throw new RangeError("deposit must be positive");
this.#balance += cents;
return this;
}
}
A composição com funções e objetos simples continua sendo uma alternativa perfeitamente boa; as classes se justificam quando identidade e estado mutável encapsulado importam.
O event loop, promises e async/await
JavaScript executa o seu código numa única thread com uma fila. O trabalho síncrono roda até o fim; tudo o que é assíncrono (um timer, uma resposta de rede, um clique) é agendado e tratado depois. Os callbacks de promises vão para a fila de microtasks, que é esvaziada por completo depois da task atual e antes do próximo timer ou etapa de renderização, então um .then() sempre roda antes de um setTimeout(..., 0) registrado antes dele. Bloquear a thread com um loop longo congela a renderização e a entrada do usuário; mova esse trabalho para um Web Worker.
Uma promise é um marcador de posição para um valor que está pendente (pending), cumprido (fulfilled) ou rejeitado (rejected). async/await é sintaxe sobre promises: uma função async sempre retorna uma promise, e await a suspende até que a promise aguardada se resolva ou seja rejeitada.
async function loadDashboard(userId) {
// Independent work — start both, then wait once.
const [profile, orders] = await Promise.all([
fetchJson(`/api/users/${userId}`),
fetchJson(`/api/users/${userId}/orders`),
]);
return { profile, orders };
}
Os combinadores importam: Promise.all rejeita assim que qualquer entrada rejeita, Promise.allSettled espera todas e informa cada resultado, Promise.any resolve com o primeiro sucesso e Promise.race assume o resultado da primeira que terminar, o que é a base de um timeout. Fazer await dentro de um loop serializa requisições que poderiam rodar em paralelo, e essa é a lentidão evitável mais comum em código assíncrono.
Erros em funções async são exceções comuns, então try/catch/finally funciona, e um throw dentro de uma delas vira uma rejeição. Lance objetos Error em vez de strings para manter a stack trace, crie subclasses de Error para falhas de domínio e use a opção cause para preservar o erro original quando for encapsulá-lo. Uma rejeição não tratada é um defeito de verdade: adicione um catch ou deixe um handler de nível superior registrá-la.
DOM, eventos e fetch
O DOM é o modelo de objetos do documento montado pelo navegador. Consulte-o com querySelector e querySelectorAll, construa nós em vez de concatenar strings de HTML e lembre-se de que atribuir dados não confiáveis a innerHTML é exatamente como acontece o cross-site scripting: textContent é o padrão seguro.
const list = document.querySelector("#orders");
const row = document.createElement("li");
row.textContent = `Order ${order.id}`;
row.dataset.orderId = order.id;
list.append(row);
// One delegated listener instead of one per row.
list.addEventListener("click", (event) => {
const item = event.target.closest("li[data-order-id]");
if (item) openOrder(item.dataset.orderId);
});
Os eventos sobem (bubbling) do alvo pelos seus ancestrais, e é isso que faz a delegação funcionar: anexe um único handler a um pai estável e ele continuará funcionando para linhas adicionadas depois. event.preventDefault() impede a ação padrão do navegador; stopPropagation() interrompe a propagação e deve ser usado com parcimônia, porque quebra outros listeners. Quando um componente adiciona listeners a objetos de vida longa, guarde o signal de um AbortController para poder removê-los todos de uma vez.
fetch retorna uma promise que só rejeita em caso de falha de rede: um HTTP 404 ou 500 ainda resolve, o que surpreende todo mundo pelo menos uma vez:
async function fetchJson(url, options = {}) {
const response = await fetch(url, {
headers: { Accept: "application/json" },
...options,
});
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText} for ${url}`);
}
return response.json();
}
Verifique sempre response.ok e decida o que cada status significa para quem chamou: a referência de códigos de status HTTP mostra quais vale a pena tentar de novo. Use AbortController para cancelar requisições em andamento quando um componente é desmontado ou quando o texto de uma caixa de busca muda. Quando estiver depurando um payload na mão, o formatador de JSON e o codificador/decodificador de URL são mais rápidos que uma sessão no console, e as ideias deste guia se aplicam diretamente a um framework: veja os fundamentos de Angular para entender como as mesmas primitivas aparecem por trás de um modelo de componentes.
Principais pontos
- Os sete primitivos são copiados por valor e todo o resto é referência; use
===, decore a lista de valores falsy e prefira??e?.a código defensivo repetitivo. constelettêm escopo de bloco; closures capturam bindings vivos, e é assim que a linguagem oferece estado privado.- O
thisde uma função comum depende de onde ela é chamada, enquanto uma arrow function o captura lexicamente: essa única regra explica a maioria das surpresas com callbacks. - Transforme dados com
map,filter,reducee os métodos mais novos sem mutaçãotoSorted/with, e copie com spread lembrando que a cópia é rasa. - O event loop esvazia as microtasks antes dos timers; use
async/awaitpara lógica sequencial ePromise.allpara trabalho independente em vez de fazer await dentro de um loop. - No navegador, prefira
textContentainnerHTML, delegue eventos a um ancestral estável e verifique sempreresponse.ok, porquefetchnão rejeita em erros HTTP.
Fontes e leituras complementares
Perguntas frequentes
- Ainda devo usar var?
- Em código novo, não. var tem escopo de função e sofre hoisting como um binding já inicializado, então vaza para fora dos blocos e permite redeclaração sem nenhum aviso. let e const têm escopo de bloco e ficam na temporal dead zone até a declaração ser executada, o que transforma toda uma classe de bugs sutis em um erro imediato. Use const por padrão, passe para let só quando realmente for reatribuir e deixe var para ler código antigo.
- Qual é a diferença entre == e ===?
- A igualdade estrita compara tipo e valor sem nenhuma conversão. A igualdade frouxa aplica antes o algoritmo de comparação abstrata, então uma string pode ser igual a um número e null é igual a undefined. As regras de conversão são tão extensas que pouca gente se lembra delas corretamente sob pressão, por isso use === em todo lugar. O único idioma que vale a pena manter é value == null, que testa null ou undefined numa única comparação.
- Por que o this vira undefined dentro do meu callback?
- Numa função comum, o this é decidido pela forma como a função é chamada, não pelo lugar onde foi escrita. Passar um método como callback o desprende do seu receptor, e aí o this vira undefined no modo estrito e no código de módulos. Defina o callback como arrow function, que captura o this lexicamente do escopo ao redor, ou faça o bind explícito do método ao repassá-lo.
- Quando usar async/await em vez de .then()?
- Prefira async/await para lógica sequencial: o código se lê de cima para baixo, funciona com try/catch comum e mantém stack traces úteis. Recorra aos combinadores de promises quando o fluxo não é sequencial: Promise.all para rodar trabalho independente em paralelo, Promise.allSettled quando você precisa de todos os resultados mesmo com falhas, Promise.race para timeouts. Fazer await dentro de um loop que poderia ser paralelo é um erro de desempenho comum.