JavaScript por dentro: a linguagem da web, do navegador ao servidor
Como o JavaScript nasceu, como os motores o executam, o que são o event loop, os protótipos e os módulos, e por que coerção, this e async ainda confundem tanta gente. Uma imersão com exemplos, erros comuns, segurança, TypeScript e o jeito como usamos a linguagem na Gothor.

Toda página que você abre no navegador executa JavaScript. O menu que se expande, a busca que sugere resultados enquanto você digita, o carrinho que atualiza sem recarregar a página: tudo isso é JavaScript. E a linguagem não ficou só no navegador. Ela roda em servidores, em ferramentas de linha de comando, em aplicativos de desktop e em boa parte das ferramentas que constroem a própria web.
Ao mesmo tempo, poucas linguagens são tão usadas e tão mal compreendidas. Muita gente escreve JavaScript há anos sem saber por que 0.1 + 0.2 não dá 0.3, por que um this "se perde" ou em que ordem um setTimeout e uma Promise vão rodar. Este artigo percorre a linguagem de dentro para fora: a história, o motor, os fundamentos que mais confundem, o modelo assíncrono, os módulos, o TypeScript e as práticas que evitam os erros mais caros.
O que é JavaScript
JavaScript é uma linguagem de programação dinâmica, de tipagem fraca, multiparadigma e executada por um motor embutido num ambiente, como um navegador ou o Node.js. Cada uma dessas palavras tem consequências:
- Dinâmica: os tipos são verificados durante a execução, e não antes. Uma variável pode guardar um número agora e um texto depois.
- Tipagem fraca: em muitas operações, a linguagem converte valores de um tipo para outro automaticamente, o que é prático e, às vezes, surpreendente.
- Multiparadigma: funções são valores comuns, que podem ser passadas e retornadas, o que permite um estilo funcional; objetos e protótipos permitem um estilo orientado a objetos.
- Uma thread de execução: o código de uma página ou de um processo Node roda numa única linha de execução, e a concorrência acontece pelo event loop, que veremos em detalhe.
Dois esclarecimentos de nome. JavaScript não tem relação técnica com Java; o nome foi uma decisão de marketing dos anos 1990. E o nome oficial do padrão é ECMAScript: JavaScript é a linguagem que implementa esse padrão, com o nome que todo mundo usa.
Uma história em dez dias, e trinta anos
Em 1995, a Netscape queria uma linguagem de script simples para o seu navegador, algo que designers e programadores iniciantes pudessem usar direto no HTML. Brendan Eich escreveu o primeiro protótipo em cerca de dez dias. A linguagem se chamou Mocha, depois LiveScript e, no lançamento, JavaScript, aproveitando a fama do Java naquele momento.

A Microsoft lançou a sua própria versão, chamada JScript, e para evitar que a web se dividisse em dialetos a Netscape levou a linguagem para padronização na Ecma International. Em 1997 saiu a primeira edição do ECMA-262. A terceira edição, de 1999, trouxe expressões regulares e tratamento de exceções, e serviu de base por uma década. Uma quarta edição muito ambiciosa foi abandonada em 2008, por falta de consenso.
A década seguinte mudou o papel da linguagem. Em 2005, o termo Ajax deu nome a páginas que buscam dados no servidor sem recarregar, e a web passou a ter aplicações de verdade. Em 2006 veio o jQuery, que domou as diferenças entre navegadores. Em 2008, o Google lançou o Chrome com o motor V8, que compilava JavaScript para código de máquina e iniciou uma corrida de desempenho entre os navegadores. Em 2009 saíram o ES5, com modo estrito e JSON nativo, e o Node.js, que levou o V8 para fora do navegador. Em 2010 nasceu o npm.
Em 2015 veio a maior atualização da história da linguagem, o ES2015 (também chamado de ES6), com classes, módulos, let e const, arrow functions, Promises, desestruturação e muito mais. Desde então, o comitê TC39, que reúne fabricantes de navegadores, empresas e especialistas, publica uma edição por ano. Cada novidade passa por estágios, de uma ideia inicial até a aprovação final, que exige implementações reais em mais de um motor.
A quase divisão da linguagem
Vale olhar com mais calma para os anos 2000, porque eles explicam muito do JavaScript de hoje. Durante a chamada guerra dos navegadores, a Netscape e a Microsoft acrescentavam recursos por conta própria, e cada uma implementava o padrão do seu jeito. Escrever um script que funcionasse nos dois navegadores exigia testes do tipo "se for o Internet Explorer, faça assim; senão, faça assado". Bibliotecas como o Prototype, o MooTools e, depois, o jQuery nasceram justamente para esconder essas diferenças.
A quarta edição do ECMAScript tentava resolver tudo de uma vez: tipos estáticos opcionais, classes, pacotes, namespaces. Era quase outra linguagem, e parte do comitê temia que ela quebrasse a web existente. Em 2008, numa reunião em Oslo, o grupo chegou a um acordo que ficou conhecido como Harmony: abandonar a quarta edição, publicar uma atualização modesta (a quinta, de 2009) e levar as ideias grandes adiante aos poucos, em edições futuras. Boa parte do que a quarta edição prometia chegou, com outra forma, no ES2015. E a regra que saiu dessa crise vale até hoje: não quebrar a web. Um recurso que funciona num site de 1998 tem de continuar funcionando, e é por isso que esquisitices como typeof null nunca foram corrigidas.
O Node.js e a virada para o servidor
Quando Ryan Dahl apresentou o Node.js, em 2009, a maior parte dos servidores web criava uma thread ou um processo para cada conexão. Uma conexão parada esperando o banco de dados ou o disco ocupava memória sem fazer nada. A proposta do Node era outra: uma única thread, que nunca fica parada esperando entrada e saída, e um laço de eventos que retoma cada trabalho quando a resposta chega. JavaScript era a linguagem ideal para isso, porque já nascera orientada a eventos no navegador e não carregava bibliotecas de entrada e saída bloqueantes.
O modelo pegou. O npm surgiu no ano seguinte, a comunidade cresceu muito rápido e, em 2014, uma divergência sobre o ritmo das versões levou a um fork, o io.js, que voltou a se juntar ao projeto principal em 2015, sob uma fundação independente. Desde então, e até a versão 26, o Node lançou uma versão nova a cada seis meses: as ímpares deixavam de receber correções seis meses depois, e as pares ganhavam suporte de longo prazo (LTS), que é o que se usa em produção, com correções críticas por cerca de 30 meses. A partir do Node.js 27, o projeto passa a lançar uma versão principal por ano, e todas viram LTS depois da fase inicial. A regra prática não muda: em produção, só versões LTS.
Uma edição por ano
Desde 2015, as edições anuais trazem poucas novidades de cada vez, o que torna a evolução previsível. Algumas das mais usadas no dia a dia:
| Edição | Destaques |
|---|---|
| ES2016 | operador ** (potência) e Array.prototype.includes |
| ES2017 | async/await, Object.entries e Object.values, padStart |
| ES2018 | espalhamento em objetos, Promise.prototype.finally, for await...of, grupos nomeados em expressões regulares |
| ES2019 | flat e flatMap, Object.fromEntries, catch sem variável |
| ES2020 | bigint, ?., ??, Promise.allSettled, import() dinâmico, globalThis |
| ES2021 | replaceAll, Promise.any, ??= e ` |
| ES2022 | await no topo do módulo, campos privados com #, .at(), Object.hasOwn, cause nos erros |
| ES2023 | toSorted, toReversed, toSpliced e with, que não alteram o array original; findLast |
| ES2024 | Object.groupBy e Map.groupBy, Promise.withResolvers |
| ES2025 | métodos de iteradores (map, filter, take direto num iterador), operações de conjunto (union, intersection), importação de JSON, RegExp.escape, Promise.try |
| ES2026 | Math.sumPrecise, Base64 e hexadecimal em Uint8Array, Error.isError, Array.fromAsync, Iterator.concat, getOrInsert em Map, acesso ao texto-fonte no JSON.parse |
A edição de 2026, a 17ª do ECMA-262, foi publicada em junho de 2026. Algumas novidades dela resolvem incômodos antigos. O Math.sumPrecise soma uma lista de números como se usasse os valores matemáticos exatos e arredonda só no fim: um laço comum que soma 1e20, 0.1 e -1e20 dá 0, porque o 0.1 se perde no meio da conta, enquanto Math.sumPrecise([1e20, 0.1, -1e20]) dá 0.1. Ele não faz milagre com o 0.1 + 0.2, porque esses literais já chegam aproximados, mas evita o acúmulo de erros em somas longas. O Uint8Array ganhou toBase64, fromBase64, toHex e fromHex, com a opção do alfabeto base64url, o que dispensa as conversões improvisadas com btoa e textos intermediários. E o map.getOrInsert(chave, padrão) devolve o valor existente ou grava e devolve o padrão, o que encurta o padrão "se não tem, cria" de contadores e caches.
O processo de estágios explica por que um recurso aparece num navegador antes de estar no padrão: os motores implementam as propostas avançadas, os desenvolvedores testam, e só então a edição seguinte as incorpora. Para quem escreve código, a pergunta prática é sempre a mesma: o ambiente em que meu código vai rodar já suporta isso?
Como uma proposta vira linguagem
O caminho de uma ideia até o padrão é público e tem etapas bem definidas, descritas no documento de processo do TC39:
| Estágio | O que significa |
|---|---|
| 0 | uma ideia, sem compromisso nenhum |
| 1 | o comitê aceita explorar o problema; a proposta precisa de um responsável (champion), de uma descrição do problema e de um repositório público |
| 2 | há uma solução preferida, com texto inicial de especificação, que ainda pode mudar bastante |
| 2.7 | a especificação está completa e revisada; falta validá-la com uma bateria de testes e protótipos |
| 3 | o desenho está fechado; o objetivo é ganhar experiência de implementação e descobrir problemas de compatibilidade com a web |
| 4 | pronta: entra na próxima edição anual |
Para chegar ao estágio 4, uma proposta precisa de duas implementações compatíveis que passem pelos testes de aceitação do Test262, a bateria oficial de testes da linguagem, além de experiência real com versões já distribuídas. É essa exigência que dá ao JavaScript a sua estabilidade: nada entra no padrão só no papel. Também explica a cautela com propostas no estágio 2 ou anterior, que podem mudar de forma ou ser abandonadas; usá-las por meio de um transpilador em código de produção é apostar numa sintaxe que talvez nunca exista.
Como o JavaScript roda: motor e ambiente
Para entender JavaScript, é preciso separar duas camadas que costumam ser confundidas: a linguagem, definida pelo ECMAScript, e o ambiente em que ela roda, que fornece tudo o que não é linguagem.

A linguagem define tipos, objetos, funções, classes, Promises e módulos. Ela não define como mostrar algo na tela, ler um arquivo ou fazer uma requisição de rede. Isso vem do ambiente:
- No navegador, o ambiente oferece o DOM (a página como objetos), eventos de mouse e teclado,
fetch, armazenamento local, Canvas e Web Workers. - No Node.js, oferece acesso a arquivos, rede, processos e variáveis de ambiente, com a biblioteca libuv cuidando da entrada e saída.
- Deno e Bun são runtimes de servidor mais novos. O Deno, criado pelo autor original do Node, executa TypeScript diretamente e exige permissões explícitas para acessar disco e rede. O Bun usa o motor JavaScriptCore e traz gerenciador de pacotes, empacotador e executor de testes embutidos.
Os motores são os programas que executam a linguagem: o V8 (Chrome, Edge, Node.js e Deno), o SpiderMonkey (Firefox) e o JavaScriptCore (Safari e Bun).
Do texto ao código de máquina
Os motores modernos não são simples interpretadores. No V8, por exemplo, o código é analisado e transformado numa árvore sintática, que vira bytecode executado por um interpretador (o Ignition). Enquanto o código roda, o motor coleta informações sobre ele: que tipos chegam em cada função, que formatos de objeto aparecem. Funções muito usadas são então compiladas para código de máquina otimizado (pelo TurboFan, entre outros níveis), com base nessas observações. Se uma suposição deixa de valer, por exemplo uma função que sempre recebeu números passa a receber textos, o motor desotimiza e volta ao bytecode.
Duas consequências práticas: código que trata dados de forma consistente (objetos criados sempre com as mesmas propriedades, na mesma ordem; funções chamadas sempre com os mesmos tipos) tende a ser mais rápido; e microtruques de desempenho raramente valem a pena, porque o motor já é muito bom em otimizar código simples e previsível.
Formas ocultas e caches em linha
Como o motor consegue acelerar uma linguagem em que qualquer objeto pode ganhar ou perder propriedades a qualquer momento? Uma das técnicas centrais são as formas ocultas (no V8, chamadas internamente de maps; em outros motores, de shapes). Cada objeto aponta para uma descrição do seu formato: quais propriedades tem, em que ordem e em que posição da memória cada uma fica. Objetos criados do mesmo jeito compartilham a mesma forma.
Com isso, o motor pode usar caches em linha: no ponto do código em que se lê pedido.total, ele lembra que, da última vez, o objeto tinha a forma X e o total estava na posição 3. Se o próximo objeto tiver a mesma forma, a leitura vira um acesso direto à memória, sem procurar nada. Um ponto do código que sempre vê a mesma forma é chamado de monomórfico e é o caso mais rápido; com algumas formas diferentes, vira polimórfico; com muitas, megamórfico, e o motor desiste do atalho.
function ponto(x, y) { return { x, y }; } // sempre o mesmo formato, na mesma ordem
const a = ponto(1, 2);
const b = ponto(3, 4); // a e b compartilham a forma
b.z = 5; // b passa a ter outra forma
delete a.x; // a costuma virar um "dicionário", de acesso mais lento
Nada disso é motivo para escrever código estranho. A lição prática é a mesma de antes: crie objetos de um mesmo tipo sempre com as mesmas propriedades (classes e funções de fábrica ajudam), prefira atribuir undefined ou null a usar delete em objetos usados em laços quentes, e não misture tipos no mesmo array quando ele for grande e muito percorrido.
Memória e coleta de lixo
A memória é gerenciada automaticamente. O coletor de lixo encontra objetos que não são mais alcançáveis a partir das variáveis em uso e libera o espaço. Por isso, vazamentos de memória em JavaScript quase sempre são referências esquecidas: um cache global que só cresce, um listener de evento que nunca é removido, uma closure que segura um objeto grande, um intervalo de tempo que nunca é cancelado.
Os coletores modernos são geracionais, porque a maior parte dos objetos morre jovem: o objeto temporário de um cálculo, a string montada para um log, o resultado intermediário de um map. No V8, os objetos novos nascem numa área pequena, a geração jovem, que é varrida com frequência e muito rapidamente: só os sobreviventes são copiados adiante. Quem sobrevive a duas varreduras é promovido à geração antiga, que é marcada e compactada com menos frequência, em boa parte do tempo em paralelo e em pequenos passos, para não congelar a página ou o servidor. Ainda assim, um programa que cria muito lixo paga em pausas e em CPU, e um processo Node tem um teto de memória para a geração antiga (ajustável com a opção --max-old-space-size).
A linguagem oferece ferramentas para guardar dados sem impedir a coleta. Um WeakMap associa informações a um objeto sem segurá-lo: quando o objeto deixa de ser usado em outro lugar, a entrada some junto. É o jeito certo de criar um cache por objeto, por exemplo os metadados de cada elemento da página.
A edição de 2021 foi além, com o WeakRef, uma referência que não impede a coleta (ref.deref() devolve o objeto ou undefined, se ele já foi coletado), e o FinalizationRegistry, que permite registrar uma função de limpeza para quando um objeto for coletado. A própria MDN recomenda evitá-los sempre que possível: quando e se a coleta acontece depende do motor, e a limpeza pode chegar muito depois do esperado, ou nunca. Servem para casos raros, como caches de objetos caros que podem ser recriados, e nunca para liberar recursos que precisam ser liberados, como conexões e arquivos, que pedem uma liberação explícita.
Como achar um vazamento
Quando a memória de uma página ou de um servidor só cresce, o caminho costuma ser este:
- Confirme o padrão. No Node,
process.memoryUsage()mostra o heap em uso; num gráfico ao longo de horas, um vazamento aparece como uma escada que nunca desce, mesmo depois das coletas. - Tire dois retratos da memória (heap snapshots), um no início e outro depois de repetir várias vezes a ação suspeita. No navegador, isso fica na aba Memory das ferramentas do desenvolvedor; no Node, basta iniciar o processo com
--inspecte conectar as mesmas ferramentas. - Compare os retratos e ordene pelo que mais cresceu. A ferramenta mostra a cadeia de referências que mantém cada objeto vivo, o que normalmente aponta o culpado: um array global, um mapa de sessões sem expiração, um evento registrado a cada abertura de tela e nunca removido.
- Corrija na origem: limite o tamanho do cache, dê prazo de validade às entradas, remova os listeners ao desmontar um componente, cancele os intervalos.
Os fundamentos que mais confundem
Tipos e coerção
JavaScript tem sete tipos primitivos (string, number, bigint, boolean, undefined, symbol e null) e os objetos, que incluem arrays e funções. Duas peculiaridades históricas aparecem logo: typeof null retorna "object", um erro da primeira versão que nunca pôde ser corrigido sem quebrar a web, e todos os números comuns são de ponto flutuante de 64 bits, o padrão IEEE 754.
Desse padrão vem o famoso 0.1 + 0.2 === 0.30000000000000004. Não é um defeito do JavaScript: acontece em qualquer linguagem que use ponto flutuante binário. Para valores monetários, trabalhe com centavos em números inteiros (ou bigint) e formate só na exibição.
A coerção é a conversão automática de tipos. Ela aparece principalmente no operador == e no +:
| Expressão | Resultado | Por quê |
|---|---|---|
'1' == 1 | true | o texto é convertido para número |
0 == '' | true | o texto vazio vira 0 |
null == undefined | true | regra especial do == |
'1' + 1 | '11' | com um texto, o + concatena |
'3' * '4' | 12 | o * só existe para números |
NaN === NaN | false | NaN não é igual a nada, nem a si mesmo |
[] + {} | '[object Object]' | os dois viram texto antes de somar |
A regra prática é simples: **use sempre === e !==**, que comparam sem converter. Para testar NaN, use Number.isNaN(valor). A única exceção aceita por muitos manuais de estilo é valor == null, que testa null e undefined de uma vez.
Três jeitos de comparar: ===, Object.is e SameValueZero
O === também tem dois casos especiais, ambos herdados do ponto flutuante: NaN === NaN é falso, e 0 === -0 é verdadeiro. Na maior parte do código isso não importa, mas a linguagem tem, na verdade, quatro algoritmos de igualdade, e cada operação usa um:
| Algoritmo | Quem usa | NaN igual a NaN? | 0 igual a -0? |
|---|---|---|---|
| igualdade frouxa | == | não | sim |
| igualdade estrita | ===, indexOf, lastIndexOf, switch | não | sim |
| SameValueZero | includes, chaves de Map e valores de Set | sim | sim |
| SameValue | Object.is | sim | não |
Daí vem uma diferença que surpreende: [NaN].indexOf(NaN) devolve -1, porque usa a igualdade estrita, enquanto [NaN].includes(NaN) devolve true. Pela mesma razão, um Set guarda no máximo um NaN, e um Map com a chave -0 é encontrado com a chave 0. O Object.is raramente é necessário; a própria documentação da MDN sugere usá-lo só quando a diferença entre 0 e -0 importa, e tratar NaN à parte com Number.isNaN no resto dos casos.
Verdadeiro, falso e os valores "vazios"
Em condições como if (valor), todo valor é tratado como verdadeiro ou falso. A lista dos valores falsos é curta e vale decorar: false, 0, -0, 0n, '' (texto vazio), null, undefined e NaN. Todo o resto é verdadeiro, inclusive '0', 'false', [] e {}.
Isso causa um erro frequente: if (quantidade) trata uma quantidade zero como se não existisse, e quantidade || 1 troca o zero por um. Quando zero ou texto vazio são valores legítimos, compare explicitamente (quantidade !== undefined) ou use o ??, que só troca null e undefined.
Valores e referências
Os primitivos são copiados por valor: let b = a com um número cria uma cópia independente. Objetos, arrays e funções são acessados por referência: const b = a com um objeto faz as duas variáveis apontarem para o mesmo objeto, e alterar b.status altera o que a enxerga.
const original = { cliente: 'Ana', itens: [{ preco: 10 }] };
const rasa = { ...original }; // copia só o primeiro nível
rasa.itens.push({ preco: 5 }); // altera também original.itens
const profunda = structuredClone(original); // copia tudo, em qualquer profundidade
profunda.itens.push({ preco: 7 }); // original continua igual
Duas consequências práticas. Primeiro, comparar objetos com === compara a referência, não o conteúdo: { a: 1 } === { a: 1 } é false. Segundo, uma função que recebe um objeto pode alterá-lo sem que quem chamou perceba. Por isso o estilo moderno favorece criar objetos novos em vez de alterar os existentes, e Object.freeze ajuda a proteger configurações, lembrando que ele também congela só o primeiro nível.
Números, dinheiro e formatação
Além do problema das casas decimais, o ponto flutuante de 64 bits tem um limite para inteiros: até Number.MAX_SAFE_INTEGER, que é 9.007.199.254.740.991, todo inteiro é representado com exatidão; acima disso, números vizinhos passam a ser confundidos. Isso importa quando um banco de dados usa identificadores de 64 bits: um id muito grande que chega como número pode ser arredondado em silêncio. A solução é trafegar esses ids como texto ou usar bigint (9007199254740993n), que tem precisão ilimitada mas não se mistura com number nas contas.
Para dinheiro, o caminho seguro é fazer as contas em centavos inteiros e arredondar uma vez, no ponto certo. Desconfie do toFixed para arredondar: ele trabalha sobre o número binário e devolve texto, por isso (1.005).toFixed(2) resulta em '1.00', e não em '1.01'. Para exibir, a API Intl resolve formato, separadores e moeda conforme o idioma:
const reais = new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' });
reais.format(1234.5); // 'R$ 1.234,50'
const centavos = 19990; // o valor guardado
reais.format(centavos / 100); // 'R$ 199,90', só na exibição
Textos e Unicode
Os textos de JavaScript são sequências de unidades de 16 bits (UTF-16). Para quase todo o português isso é transparente, mas alguns caracteres, como muitos emojis, ocupam duas unidades: '😀'.length é 2. Para contar caracteres como uma pessoa contaria, use [...texto].length, que percorre o texto por caractere.
Acentos têm outra pegadinha: "ã" pode ser gravado como um caractere só ou como "a" seguido de um til combinável. Os dois aparecem iguais na tela e são diferentes para o ===. O método normalize resolve: normalize('NFC') junta tudo na forma composta, e normalize('NFD') separa as letras dos acentos, o que permite removê-los com uma expressão regular. É assim que muitos sistemas geram endereços amigáveis a partir de títulos, transformando "Boleto híbrido" em boleto-hibrido. Para ordenar nomes em português, use localeCompare ou Intl.Collator('pt-BR'), que sabem que "Álvaro" vem antes de "Bruno".
Expressões regulares modernas
As expressões regulares do JavaScript ficaram muito mais capazes desde a edição de 2018, e muito código antigo ainda as usa como se fosse 2009. Alguns recursos que valem conhecer:
- Grupos nomeados (ES2018):
(?<ano>\d{4})-(?<mes>\d{2})permite lerresultado.groups.anoem vez de contar parênteses. Desde o ES2025, o mesmo nome pode aparecer em alternativas diferentes da expressão. - Lookbehind (ES2018):
(?<=R\$ )\d+encontra o número só quando vem depois de "R$ ", sem incluir o prefixo no resultado. - **A flag
s** (ES2018) faz o ponto aceitar também quebras de linha, e a flag **d** (ES2022) devolve as posições de início e fim de cada grupo, útil para destacar trechos num editor. - Propriedades Unicode: com a flag
u,\p{L}significa "qualquer letra, de qualquer alfabeto", o que inclui "ç" e "ã", ao contrário de[a-zA-Z]. A flag **v** (ES2024) amplia isso com operações de conjunto dentro das classes, como a interseção[\p{L}&&\p{Script=Latin}](letras do alfabeto latino) e a subtração[\p{L}--[a-z]]. As duas flags são incompatíveis entre si: usaruevjuntas é erro de sintaxe. - **
RegExp.escape** (ES2025) escapa um texto para que ele seja procurado literalmente. Montar uma expressão com o que a pessoa digitou na busca, sem escapar, transforma um "R$ 10,00" num padrão com significado especial para o$.
const data = /(?<dia>\d{2})\/(?<mes>\d{2})\/(?<ano>\d{4})/;
const { dia, mes, ano } = '20/10/2026'.match(data).groups;
`${ano}-${mes}-${dia}`; // '2026-10-20'
const termo = 'R$ 10,00';
new RegExp(RegExp.escape(termo)).test('Total: R$ 10,00'); // true
A recomendação de segurança, que aparece adiante, vale para todas: expressões com quantificadores aninhados podem levar um tempo exponencial com certas entradas.
Datas e fusos horários
O objeto Date é uma das partes mais antigas e mais criticadas da linguagem. Ele é mutável, os meses começam em zero (new Date(2026, 0, 15) é 15 de janeiro) e o mesmo texto pode ser lido de formas diferentes: new Date('2026-10-20') é interpretado como meia-noite em UTC, e para quem está em Cuiabá, quatro horas atrás, essa data aparece como 19 de outubro às 20h.
Algumas regras evitam a maioria dos problemas: guarde e transmita datas em UTC, no formato ISO 8601 (2026-10-20T10:00:00Z); converta para o fuso de quem lê só na exibição; e use Intl.DateTimeFormat com o fuso explícito quando o servidor e o usuário estão em lugares diferentes:
new Intl.DateTimeFormat('pt-BR', {
dateStyle: 'short',
timeStyle: 'short',
timeZone: 'America/Cuiaba',
}).format(new Date('2026-10-20T10:00:00Z')); // '20/10/2026, 06:00'
A proposta Temporal, do TC39, traz tipos imutáveis e separados para cada caso (uma data sem hora, uma hora sem data, um instante, uma data com fuso) e já começou a chegar aos navegadores. Ela chegou ao estágio 4 e está prevista para a edição de 2027 do padrão. Os nomes dos tipos dizem o que cada um guarda: Temporal.PlainDate é uma data de calendário, como um vencimento; Temporal.PlainTime é uma hora do dia, como o horário de abertura da loja; Temporal.Instant é um ponto exato no tempo, sem fuso; Temporal.ZonedDateTime junta um instante, um fuso e um calendário; e Temporal.Duration representa um intervalo, como "3 dias e 4 horas".
const vencimento = Temporal.PlainDate.from('2026-11-10');
vencimento.add({ days: 30 }).toString(); // '2026-12-10', sem horas nem fusos no caminho
const pedido = Temporal.ZonedDateTime.from('2026-10-20T10:00:00[America/Cuiaba]');
pedido.add({ hours: 3 }).toString(); // '2026-10-20T13:00:00-04:00[America/Cuiaba]'
A MDN ainda classifica o Temporal como de disponibilidade limitada, porque ele não funciona em alguns dos navegadores mais usados. Até ele estar em todo lugar, há polyfills, como o @js-temporal/polyfill, e bibliotecas como a date-fns cobrem as contas com datas.
Escopo, hoisting e closures
Há três formas de declarar variáveis. O var, a mais antiga, tem escopo de função e é "içado" (hoisting) para o topo da função, podendo ser usado antes da declaração, com o valor undefined. O let e o const, de 2015, têm escopo de bloco e não podem ser usados antes da declaração. O const impede reatribuir a variável, mas não impede alterar o objeto para o qual ela aponta.
Uma closure é uma função que lembra as variáveis do lugar onde foi criada, mesmo depois que esse lugar terminou de executar. É um dos recursos mais poderosos da linguagem:
function criarContador() {
let total = 0; // só a função de dentro enxerga esta variável
return () => ++total;
}
const proximo = criarContador();
proximo(); // 1
proximo(); // 2
Closures dão privacidade sem classes, são a base de callbacks e de hooks como os do React e explicam um erro clássico. Com var, só existe uma variável para o laço inteiro:
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i)); // 3, 3, 3
for (let j = 0; j < 3; j++) setTimeout(() => console.log(j)); // 0, 1, 2
Com let, cada volta do laço tem a sua própria variável, e cada closure guarda a sua. Na prática: use const por padrão, let quando precisar reatribuir e não use var em código novo.
O valor de this
Em JavaScript, this não depende de onde a função foi escrita, e sim de como foi chamada:
- Chamada como método (
pedido.total()),thisé o objeto antes do ponto. - Chamada solta (
const f = pedido.total; f()),thiséundefinedem modo estrito, e o método "perde" o objeto. - Chamada com
new,thisé o objeto novo. - Com
call,applyoubind,thisé o que você indicar.
As arrow functions são a exceção: elas não têm this próprio e usam o do lugar onde foram criadas. Por isso são ideais para callbacks dentro de métodos e inadequadas como métodos de objetos que precisam do próprio this. Quando um método é passado como callback, por exemplo para um evento, use uma arrow function ou bind para não perder o objeto.
O modo estrito
A regra do this como undefined numa chamada solta vale no modo estrito, uma variante mais rigorosa da linguagem criada no ES5 e ligada pela linha 'use strict' no topo de um arquivo ou de uma função. Fora dele, o this de uma chamada solta é o objeto global, e um método que "perde" o objeto passa a ler e gravar propriedades no lugar errado, sem erro nenhum.
O modo estrito transforma vários erros silenciosos em erros de verdade:
- Atribuir a uma variável não declarada lança
ReferenceError, em vez de criar uma variável global por acidente. Um erro de digitação comototl = 10deixa de passar despercebido. - Gravar numa propriedade somente leitura, num objeto congelado com
Object.freezeou numa propriedade que só tem getter lançaTypeError, em vez de ser ignorado. - Parâmetros com nomes repetidos, números octais no formato antigo (
015) e o comandowithviram erros de sintaxe. - O
evaldeixa de criar variáveis no escopo de quem o chama, e o objetoargumentsdeixa de espelhar os parâmetros.
Na prática, quase todo código moderno já roda em modo estrito sem escrever nada: o conteúdo de um módulo ES e o corpo de uma classe são estritos automaticamente. A linha 'use strict' só continua relevante em scripts clássicos e em arquivos CommonJS. Essa é mais uma razão para preferir módulos ES em código novo: a categoria de erros que o modo estrito revela simplesmente não aparece.
Protótipos e classes
JavaScript implementa herança por delegação: todo objeto tem um elo interno, o [[Prototype]], para outro objeto. Quando você lê uma propriedade que o objeto não tem, o motor procura no protótipo, depois no protótipo do protótipo, até chegar a null.

A sintaxe de class, de 2015, é uma forma mais clara de escrever exatamente isso. Os métodos de uma classe ficam no protótipo, compartilhados por todas as instâncias, e não copiados em cada objeto:
class Pedido {
#desconto = 0; // campo privado: só acessível dentro da classe
constructor(valor) { this.valor = valor; }
total() { return this.valor - this.#desconto; }
aplicarDesconto(v) { this.#desconto = v; }
}
const p = new Pedido(100);
Object.getPrototypeOf(p) === Pedido.prototype; // true
p.hasOwnProperty('total'); // false: total mora no protótipo
Os campos com # são privados de verdade, garantidos pela linguagem, e não apenas uma convenção.
Getters, setters e Proxy
Uma propriedade não precisa guardar um valor: ela pode ser calculada na hora da leitura. Os acessores get e set permitem que pedido.total execute uma função, sem que quem lê perceba a diferença:
class Carrinho {
#itens = [];
#desconto = 0;
adicionar(item) { this.#itens.push(item); }
get total() {
const bruto = this.#itens.reduce((s, i) => s + i.precoCentavos * i.qtd, 0);
return Math.round(bruto * (1 - this.#desconto / 100)); // calculado a cada leitura
}
set desconto(v) { // valida antes de aceitar
if (v < 0 || v > 100) throw new RangeError('Desconto entre 0 e 100');
this.#desconto = v;
}
}
O **Proxy**, do ES2015, leva a ideia ao extremo: ele envolve um objeto e intercepta as operações sobre ele (ler, gravar, apagar, listar chaves, verificar com in) por meio de funções chamadas de armadilhas (traps). O objeto **Reflect** oferece as operações padrão correspondentes, para que a armadilha faça o seu trabalho e depois repasse ao comportamento normal.
const registrarMudancas = (alvo, aoMudar) => new Proxy(alvo, {
set(obj, chave, valor, receptor) {
aoMudar(chave, valor);
return Reflect.set(obj, chave, valor, receptor); // segue o comportamento normal
},
});
const filtro = registrarMudancas({ busca: '' }, (k, v) => console.log(k, v));
filtro.busca = 'boleto'; // imprime: busca boleto
Esse mecanismo é a base da reatividade de frameworks como o Vue 3: o reactive() devolve um Proxy cuja armadilha de leitura registra quem leu cada propriedade e cuja armadilha de escrita avisa esses leitores para se atualizarem. O Vue 2, por limitações de suporte dos navegadores da época, fazia o mesmo só com getters e setters. O custo é que cada operação num Proxy passa por uma função, o que o torna mais lento que um objeto comum em laços muito quentes, e que um Proxy não é o mesmo objeto que o original para o ===.
Funções, coleções e iteração
Funções como valores
Em JavaScript, uma função é um valor como outro qualquer: pode ser guardada numa variável, passada como argumento e devolvida por outra função. Funções que recebem ou devolvem funções são chamadas de funções de ordem superior, e são elas que dão ao código moderno o seu estilo declarativo. Em vez de um laço com índice, variável acumuladora e condições, descreve-se o que se quer:
const pedidos = [
{ id: 1, status: 'pago', total: 120 },
{ id: 2, status: 'aberto', total: 80 },
{ id: 3, status: 'pago', total: 45 },
];
const pagos = pedidos.filter((p) => p.status === 'pago'); // escolher
const totais = pagos.map((p) => p.total); // transformar
const faturado = totais.reduce((soma, t) => soma + t, 0); // juntar: 165
const temGrande = pedidos.some((p) => p.total > 100); // existe algum? true
const primeiroAberto = pedidos.find((p) => p.status === 'aberto'); // o primeiro que atende
const porStatus = Object.groupBy(pedidos, (p) => p.status); // { pago: [...], aberto: [...] }
O cuidado é não encadear transformações em arrays enormes sem necessidade, porque cada map e cada filter criam um array novo. Para listas de milhares de itens isso é irrelevante; para milhões, um laço simples ou os métodos de iteradores, que processam um item de cada vez, economizam memória.
Map, Set e objetos simples
Por muito tempo, objetos simples foram usados como dicionários. Funciona, mas com limitações: as chaves viram texto, existe o risco de colidir com propriedades herdadas como toString, e não há um jeito direto de saber o tamanho. O **Map** é feito para isso: aceita qualquer valor como chave, mantém a ordem de inserção, tem size e é otimizado para inclusões e remoções frequentes. O **Set** guarda valores sem repetição, o que torna trivial remover duplicados ([...new Set(lista)]) e, desde o ES2025, calcular uniões e interseções.
A regra prática: objeto simples para registros com campos conhecidos (um pedido, um cliente); Map para coleções com chaves variáveis (um cache por código de cliente, uma contagem por produto); Set para "já vi este item?".
Iteradores e geradores
O for...of, o espalhamento (...) e a desestruturação funcionam com qualquer objeto iterável, isto é, que saiba entregar os seus itens um a um. Arrays, textos, Map e Set já são iteráveis, e qualquer objeto pode ser.
O jeito mais simples de criar uma sequência é um gerador, uma função marcada com * que entrega valores com yield e pausa entre um e outro. Nada é calculado antes da hora:
function* paginas(total, porPagina) {
for (let inicio = 0; inicio < total; inicio += porPagina) {
yield { inicio, fim: Math.min(inicio + porPagina, total) };
}
}
for (const p of paginas(95, 30)) console.log(p);
// { inicio: 0, fim: 30 } ... { inicio: 90, fim: 95 }
Os geradores assíncronos (async function*) combinam as duas ideias: produzem valores que chegam com o tempo, consumidos com for await...of. São a ferramenta natural para dados que chegam aos pedaços, como as linhas de um arquivo grande, as páginas de uma API ou o texto de uma resposta de IA sendo gerado em tempo real.
Assíncrono de verdade: o event loop
Se o JavaScript tem uma só thread, como uma página continua respondendo enquanto espera uma requisição de rede, ou como um servidor Node atende milhares de conexões? A resposta é que o JavaScript não espera. Tudo o que demora (temporizadores, rede, disco) é entregue ao ambiente, e o código segue em frente. Quando a espera termina, o ambiente coloca um callback numa fila, e o event loop o executa quando a pilha de chamadas estiver vazia.

Há duas filas, com prioridades diferentes. A fila de tarefas recebe callbacks de temporizadores, eventos e entrada e saída. A fila de microtarefas recebe as continuações de Promises e o que vem depois de um await. A regra do laço é: executar o código síncrono até a pilha esvaziar; esvaziar todas as microtarefas; pegar uma tarefa da fila; repetir. No navegador, a tela pode ser redesenhada entre uma tarefa e outra.
Com isso, a ordem do exemplo clássico deixa de ser mistério:
console.log('1: síncrono');
setTimeout(() => console.log('4: tarefa'), 0);
Promise.resolve().then(() => console.log('3: microtarefa'));
console.log('2: síncrono');
// 1: síncrono
// 2: síncrono
// 3: microtarefa
// 4: tarefa
O setTimeout com zero milissegundos não executa "agora": executa na próxima tarefa, depois de todo o código síncrono e de todas as microtarefas.
Um exemplo com await, passo a passo
O await também segue essas regras: ele pausa a função e agenda o restante dela como uma microtarefa. Um exemplo um pouco mais difícil:
async function carregar() {
console.log('A1');
await null;
console.log('A2');
}
console.log('S1');
setTimeout(() => console.log('T1'));
carregar();
Promise.resolve().then(() => console.log('P1'));
console.log('S2');
| Passo | O que acontece | Saída |
|---|---|---|
| 1 | O script imprime S1 | S1 |
| 2 | O setTimeout entrega o callback ao ambiente, que o põe na fila de tarefas | |
| 3 | carregar() começa a rodar de forma síncrona e imprime A1 | A1 |
| 4 | O await pausa a função e agenda o resto dela como microtarefa | |
| 5 | O .then agenda P1 como segunda microtarefa | |
| 6 | O script imprime S2 e a pilha esvazia | S2 |
| 7 | O laço esvazia as microtarefas, na ordem: o resto de carregar e o P1 | A2, P1 |
| 8 | Só então pega a tarefa do temporizador | T1 |
A ordem final é S1, A1, S2, A2, P1, T1. O ponto que mais surpreende é o passo 3: uma função async não é "assíncrona desde o início"; ela roda normalmente até o primeiro await.
As fases do event loop no Node
No Node, o laço é implementado pela libuv e passa por fases, cada uma com a sua fila:
| Fase | O que roda |
|---|---|
| timers | callbacks de setTimeout e setInterval cujo prazo venceu |
| callbacks pendentes | alguns retornos de operações do sistema adiados da volta anterior |
| poll | espera por entrada e saída e roda os callbacks de rede e disco |
| check | callbacks de setImmediate |
| close | eventos de fechamento, como o de um socket |
Entre um callback e outro, em qualquer fase, o Node esvazia primeiro a fila do process.nextTick e depois a das microtarefas de Promise. Por isso um setImmediate agendado dentro de um callback de entrada e saída roda sempre antes de um setTimeout de zero: a fase de check vem logo depois da de poll, e a de timers só na volta seguinte.
Cancelar e limitar o tempo
Uma requisição que nunca responde é pior do que uma que falha: ela segura memória, conexões e, às vezes, a paciência de quem espera. A plataforma oferece um mecanismo padrão de cancelamento, o **AbortController**, aceito pelo fetch, por várias APIs do Node e por bibliotecas:
const controle = new AbortController();
botaoCancelar.onclick = () => controle.abort();
const resposta = await fetch('/api/relatorio', {
// cancela se a pessoa clicar ou se passar de 10 segundos, o que vier primeiro
signal: AbortSignal.any([controle.signal, AbortSignal.timeout(10_000)]),
});
AbortSignal.timeout cria um sinal que dispara sozinho depois do prazo, e AbortSignal.any combina vários sinais num só. Quem recebe o sinal deve repassá-lo a tudo o que chamar por baixo, para que um cancelamento lá em cima pare o trabalho inteiro, e não só a primeira etapa.
Streams: dados que chegam aos pedaços
Nem toda resposta deve ser esperada inteira. Um arquivo de 2 GB, um relatório gerado linha a linha ou o texto de um assistente de IA sendo escrito palavra por palavra chegam aos poucos, e processá-los conforme chegam economiza memória e mostra resultado mais cedo. As Web Streams (ReadableStream, WritableStream, TransformStream) são o padrão para isso, e hoje existem no navegador, no Node, no Deno e no Bun. O corpo de uma resposta do fetch é um ReadableStream:
const resposta = await fetch('/api/exportacao');
const leitor = resposta.body.getReader();
const decodificador = new TextDecoder();
for (;;) {
const { done, value } = await leitor.read();
if (done) break;
// stream: true evita quebrar um caractere acentuado dividido entre dois pedaços
mostrar(decodificador.decode(value, { stream: true }));
}
Um detalhe que só aparece em produção: os pedaços da rede não respeitam linhas nem caracteres. Um "ç" pode chegar com metade dos bytes num pedaço e metade no outro, e uma linha de dados pode ser cortada no meio. Por isso o decodificador recebe stream: true, e quem lê linhas guarda o resto num acumulador até a linha se completar. Outro conceito importante é a contrapressão (backpressure): quando quem consome é mais lento que quem produz, a stream sinaliza para a origem diminuir o ritmo, em vez de acumular tudo na memória. No Node, a função pipeline, do módulo node:stream, liga as etapas cuidando disso e dos erros.
De callbacks a async/await
O código assíncrono passou por três gerações. Primeiro vieram os callbacks, funções passadas para serem chamadas no fim da operação, que em fluxos longos viravam pirâmides difíceis de ler e de tratar erros. Depois vieram as Promises, objetos que representam um resultado futuro, encadeáveis com .then e .catch. Por fim, async/await, de 2017, permitiu escrever código assíncrono que se lê de cima para baixo:
async function carregarPainel(clienteId) {
try {
// as duas requisições correm em paralelo; esperamos as duas juntas
const [cliente, pedidos] = await Promise.all([
fetch(`/api/clientes/${clienteId}`).then((r) => r.json()),
fetch(`/api/clientes/${clienteId}/pedidos`).then((r) => r.json()),
]);
return { cliente, pedidos };
} catch (erro) {
console.error('Falha ao carregar o painel', erro);
throw erro;
}
}
Algumas regras evitam a maior parte dos problemas:
- **Não esqueça o
await.** Sem ele, você recebe uma Promise pendente, e um erro dentro dela pode passar em silêncio. - Paralelize o que é independente. Dois
awaitseguidos esperam um depois do outro.Promise.allespera os dois juntos;Promise.allSettledespera todos sem falhar no primeiro erro. - **Cuidado com
forEache async.** OforEachnão espera Promises. Usefor...ofcomawaitpara executar em sequência ouPromise.allcommappara executar em paralelo. - Trate a rejeição. Toda Promise deve ter um
catchem algum ponto da cadeia. No Node, uma rejeição não tratada encerra o processo.
Quando o event loop trava
O modelo de uma thread tem um limite claro: código síncrono pesado bloqueia tudo. Um laço que processa um arquivo enorme, um cálculo longo ou uma expressão regular mal escrita congelam a página ou deixam o servidor sem responder a nenhuma requisição enquanto rodam. Microtarefas que geram novas microtarefas sem fim têm o mesmo efeito, porque o laço só segue depois de esvaziar essa fila. Para trabalho pesado de CPU, use Web Workers no navegador e worker threads no Node, que rodam em outra thread e conversam por mensagens.
Paralelismo de verdade: workers e memória compartilhada
Um worker é um segundo ambiente JavaScript completo, com o seu próprio event loop e a sua própria memória. Ele não enxerga as variáveis da página nem o DOM. A comunicação acontece por postMessage, e o que é enviado passa pelo algoritmo de clonagem estruturada, o mesmo do structuredClone: o worker recebe uma cópia, e não uma referência. Isso elimina por construção as disputas de memória entre threads, que são a fonte de erros mais difícil de outras linguagens.
// pagina.js
const trabalhador = new Worker(new URL('./conciliar.js', import.meta.url), { type: 'module' });
trabalhador.postMessage({ lancamentos }); // enviado como cópia
trabalhador.onmessage = (e) => mostrar(e.data.divergencias);
// conciliar.js
self.onmessage = (e) => {
const divergencias = conciliar(e.data.lancamentos); // trabalho pesado, fora da thread da tela
self.postMessage({ divergencias });
};
Copiar tem um custo quando os dados são grandes. Para isso existem os objetos transferíveis: um ArrayBuffer passado na lista de transferência muda de dono em vez de ser copiado, e deixa de poder ser usado por quem enviou. E, quando duas threads precisam mesmo trabalhar sobre a mesma memória, existe o **SharedArrayBuffer**, com o objeto **Atomics** para ler, gravar e esperar de forma sincronizada.
A memória compartilhada tem uma história ligada à segurança. No início de 2018, depois da descoberta das vulnerabilidades de processador conhecidas como Spectre, os navegadores desligaram o SharedArrayBuffer junto com os temporizadores de alta resolução, já que medir o tempo com muita precisão é parte desse tipo de ataque. Em 2020, foi padronizada uma forma segura de religá-lo, com uma condição: a página precisa estar isolada de outras origens, o que se consegue com dois cabeçalhos HTTP, Cross-Origin-Opener-Policy: same-origin e Cross-Origin-Embedder-Policy: require-corp (ou credentialless). A propriedade crossOriginIsolated diz, em execução, se a página cumpre a condição. Sem ela, o construtor nem aparece no objeto global.
Para a maior parte das aplicações, mensagens e transferências bastam. Memória compartilhada faz sentido em casos específicos, como processamento de imagem, áudio e cálculos numéricos pesados, frequentemente junto com WebAssembly.
No Node, vale conhecer duas particularidades: process.nextTick agenda um callback para antes até das microtarefas de Promise, e setImmediate agenda para a fase seguinte do laço, depois da entrada e saída. Na dúvida, prefira Promises e queueMicrotask, que funcionam igual nos dois ambientes.
Erros e exceções
JavaScript permite lançar qualquer valor com throw, inclusive um texto, mas só objetos Error carregam a pilha de chamadas (stack trace), que mostra onde o problema nasceu. Lance sempre um Error ou uma classe derivada dele. A linguagem já traz alguns tipos: TypeError (usar um valor do jeito errado, como chamar undefined como função), ReferenceError (variável que não existe), RangeError (valor fora do intervalo aceito) e SyntaxError (código ou JSON inválido).
Para regras de negócio, vale criar classes próprias. Elas permitem que quem trata o erro decida pelo tipo, e não comparando mensagens:
class EstoqueInsuficiente extends Error {
constructor(produto, pedido, disponivel, opcoes) {
super(`Estoque insuficiente de ${produto}: pedido ${pedido}, disponível ${disponivel}`, opcoes);
this.name = 'EstoqueInsuficiente';
}
}
try {
await reservar(item);
} catch (erro) {
if (erro instanceof EstoqueInsuficiente) avisarComprador(erro.message);
else throw new Error('Falha ao reservar o item', { cause: erro }); // preserva a origem
}
A opção cause, de 2022, encadeia erros sem perder o original: o log mostra a falha de alto nível ("falha ao reservar") e, logo abaixo, a de baixo nível ("conexão recusada pelo banco"). Três hábitos completam o quadro. Só capture um erro onde você sabe o que fazer com ele; capturar e ignorar esconde defeitos. Use finally para liberar recursos (fechar um arquivo, esconder um indicador de carregamento) aconteça o que acontecer. E mostre ao usuário uma mensagem compreensível, deixando os detalhes técnicos para o log.
Nos ambientes, existem redes de segurança para o que escapa: o evento unhandledrejection no navegador e o process.on('unhandledRejection') no Node permitem registrar rejeições que ninguém tratou. Elas servem para registrar e investigar, não para substituir o tratamento no lugar certo.
Uma novidade da edição de 2026 resolve um caso de borda antigo. O instanceof Error falha com erros criados em outro realm, como um iframe, porque cada um tem a sua própria classe Error, e aceita objetos comuns que só imitam um erro pela cadeia de protótipos. O Error.isError(valor) confere uma marca interna que só o construtor de Error coloca, do mesmo jeito que o Array.isArray faz com arrays, e responde se o valor é um erro de verdade, venha de onde vier.
Liberar recursos com using
O finally resolve a liberação de um recurso, mas fica repetitivo quando há vários: abrir um arquivo, pegar uma conexão do pool, adquirir uma trava, e soltar tudo na ordem inversa, mesmo que algo falhe no meio. A proposta de gerenciamento explícito de recursos chegou ao estágio 4 e está prevista para a edição de 2027. Ela cria a declaração using: quando o bloco termina, por qualquer caminho, o motor chama o método [Symbol.dispose]() do valor.
async function exportarRelatorio(pool) {
await using conexao = await pool.conectar(); // liberada com Symbol.asyncDispose
using arquivo = abrirTemporario(); // liberado com Symbol.dispose
// ... consultas e gravações; um erro aqui não deixa nada aberto
} // primeiro o arquivo, depois a conexão: ordem inversa à da declaração
As regras são poucas. Uma variável using funciona como um const, com escopo de bloco, e pode valer null ou undefined, para recursos opcionais. Com várias declarações, a liberação acontece na ordem inversa, como numa pilha. Se o bloco e a liberação lançarem erros, nenhum se perde: eles chegam juntos num SuppressedError, com o erro mais recente em error e o anterior em suppressed. O DisposableStack cobre os casos em que a liberação é decidida em tempo de execução, com o método defer para registrar funções de limpeza. A MDN ainda não classifica o recurso como disponível em todos os navegadores principais, e o TypeScript o aceita desde a versão 5.2, convertendo a sintaxe para versões anteriores da linguagem; os símbolos e o DisposableStack ainda dependem de polyfills onde o ambiente não os traz.
Módulos: CommonJS e ES Modules
Por muito tempo, JavaScript não tinha módulos na linguagem. Os scripts compartilhavam o escopo global, e cada comunidade inventou a sua solução. A que mais se difundiu foi o CommonJS, adotado pelo Node em 2009. Em 2015, a linguagem ganhou o seu sistema oficial, os ES Modules.

A diferença não é só de sintaxe. No CommonJS, require é uma função comum, executada na hora e de forma síncrona, que pode aparecer em qualquer lugar do código. Nos ES Modules, import e export formam uma estrutura estática, conhecida antes de o código rodar. Isso permite que ferramentas eliminem código não usado (tree shaking), que o carregamento seja assíncrono e que um módulo use await no nível superior. Os ES Modules funcionam igual no navegador e no Node.
No Node, o tipo de módulo é escolhido pela extensão (.mjs para ES Modules, .cjs para CommonJS) ou pelo campo "type": "module" no package.json. Para código novo, a recomendação é usar ES Modules.
Importação dinâmica e divisão do código
Além do import estático, existe o import() como função, que carrega um módulo sob demanda e devolve uma Promise. No navegador, isso permite dividir o código (code splitting): a tela de relatórios, com a sua biblioteca de gráficos, só é baixada quando alguém abre os relatórios. Ferramentas como o Vite fazem essa divisão automaticamente a partir de cada import(), gerando arquivos separados que o navegador baixa quando precisa.
A convivência entre os dois sistemas
A transição de CommonJS para ES Modules durou anos e ainda deixa marcas. Um módulo ES pode importar um módulo CommonJS sem problemas. O caminho contrário, um require de um módulo ES, só passou a funcionar sem opções especiais nas versões mais recentes do Node (a partir da 22.12, com suporte também na linha 20, desde a 20.19), e ainda tem uma restrição: o módulo importado não pode usar await no topo. Pacotes publicados no npm usam o campo exports do package.json para declarar o que oferecem a cada sistema e quais arquivos podem ser importados de fora, o que também impede que alguém dependa de arquivos internos do pacote.
JavaScript moderno em poucos exemplos
As edições anuais trouxeram recursos que deixam o código mais curto e mais seguro. Alguns dos mais úteis no dia a dia:
const pedido = { id: 7, cliente: { nome: 'Ana' }, itens: [{ preco: 10 }, { preco: 25 }] };
const { id, cliente } = pedido; // desestruturação
const copia = { ...pedido, status: 'pago' }; // cópia rasa com espalhamento
const cidade = pedido.cliente?.endereco?.cidade; // encadeamento opcional: undefined, sem erro
const frete = pedido.frete ?? 15; // ?? só troca null e undefined (0 continua 0)
const total = pedido.itens.reduce((s, i) => s + i.preco, 0);
const caros = pedido.itens.filter((i) => i.preco > 20);
const ultimo = pedido.itens.at(-1); // índice negativo conta do fim
const ordenados = pedido.itens.toSorted((a, b) => a.preco - b.preco); // não altera o original
const independente = structuredClone(pedido); // cópia profunda
const mensagem = `Pedido ${id} de ${cliente.nome}: R$ ${total}`;
O operador ?? merece destaque. Diferente do ||, ele não troca valores como 0, '' e false, que muitas vezes são válidos. Um frete zero continua zero.
TypeScript: JavaScript com tipos
O TypeScript, criado pela Microsoft em 2012, é JavaScript com um sistema de tipos opcional por cima. Em julho de 2026, a versão 7.0 trouxe o compilador portado para Go, cerca de dez vezes mais rápido que o anterior. Você descreve o formato dos dados, e o compilador aponta erros antes de o código rodar: uma propriedade que não existe, um argumento do tipo errado, um caso esquecido.
interface Pedido {
id: number;
status: 'aberto' | 'pago' | 'cancelado';
frete?: number;
}
function podeCancelar(p: Pedido): boolean {
return p.status === 'aberto';
}
podeCancelar({ id: 7, status: 'pendente' });
// erro: "pendente" não é um status válido, detectado antes de rodar
O ponto fundamental é que os tipos existem só antes da execução. O compilador verifica e depois remove as anotações, gerando JavaScript comum. Por isso, dados que chegam de fora (uma requisição, um arquivo, uma API) precisam ser validados durante a execução, com bibliotecas de validação como o Zod; o tipo declarado não garante nada sobre o que realmente chegou. Versões recentes do Node já executam arquivos TypeScript diretamente, removendo as anotações na hora, mas a verificação de tipos continua sendo feita pelo compilador.
Em projetos que vão crescer ou que têm mais de uma pessoa, o TypeScript costuma se pagar rapidamente: o editor passa a completar e navegar pelo código com precisão, e refatorações grandes ficam seguras.
Os recursos de tipos que mais ajudam
O sistema de tipos do TypeScript é estrutural: dois tipos com o mesmo formato são compatíveis, tenham o nome que tiverem, o que combina com o jeito como JavaScript trata objetos. Alguns recursos rendem mais do que outros no dia a dia:
- Uniões discriminadas. Um campo comum diz qual variante o objeto é, e o compilador estreita o tipo em cada ramo de um
ifouswitch. Se um caso novo for criado e esquecido em algumswitch, o compilador aponta. - **
unknownem vez deany.** Oanydesliga a verificação; ounknownobriga a conferir o tipo antes de usar. Dados que chegam de fora devem entrar comounknowne sair da validação com o tipo certo. - Genéricos. Permitem escrever uma função que funciona com qualquer tipo sem perder a informação:
primeiro<T>(lista: T[]): T | undefineddevolve umPedidoquando recebe pedidos. - **
satisfies.** Confere se um valor atende a um tipo sem apagar os detalhes do valor, útil em objetos de configuração. - Modo estrito. A opção
strictliga um conjunto de verificações, sendo a mais importante a de nulos: o compilador obriga a tratar o caso em que um valor pode sernullouundefined, a origem de boa parte dos erros em produção.
A validação na fronteira fecha o ciclo. Com o Zod, o mesmo esquema valida os dados durante a execução e gera o tipo, de modo que os dois nunca divergem:
import { z } from 'zod';
const NovoPedido = z.object({
clienteId: z.number().int().positive(),
itens: z.array(z.object({ produtoId: z.number(), quantidade: z.number().min(1) })).min(1),
observacao: z.string().max(500).optional(),
});
type NovoPedido = z.infer<typeof NovoPedido>; // o tipo sai do esquema
const resultado = NovoPedido.safeParse(corpoDaRequisicao); // corpoDaRequisicao: unknown
if (!resultado.success) responderErro(400, resultado.error.issues);
else criarPedido(resultado.data); // aqui, resultado.data é NovoPedido
O ecossistema: npm, frameworks e ferramentas
O npm é o gerenciador de pacotes do Node e o maior registro de pacotes de software do mundo. Cada projeto declara as dependências no package.json, com faixas de versão no padrão de versionamento semântico, e o package-lock.json registra as versões exatas instaladas. Em servidores de integração contínua, npm ci instala exatamente o que está no lockfile, o que torna o build reproduzível.
Em volta da linguagem existe um ecossistema enorme:
- Interfaces: React, Vue, Angular e Svelte, entre outros.
- Servidores: Express, Fastify e NestJS, este último com módulos, injeção de dependência e TypeScript como padrão.
- Build e desenvolvimento: Vite, esbuild, Rollup e webpack, que empacotam, minificam e servem o código.
- Qualidade: ESLint para análise estática, Prettier para formatação, Jest e Vitest para testes, Playwright para testes no navegador.
A abundância tem um custo: cada dependência é código de terceiros rodando com os mesmos privilégios que o seu. Instalar com lockfile, revisar o que entra, preferir pacotes mantidos e usar os recursos nativos quando eles existem (hoje fetch, structuredClone, crypto.randomUUID e muito mais já vêm prontos) reduz o risco de ataques na cadeia de suprimentos.
Segurança no JavaScript
Os riscos mais comuns têm defesas conhecidas.
Injeção de scripts (XSS). Acontece quando um texto vindo do usuário é inserido na página como HTML e o navegador executa o que estiver nele. A defesa principal é nunca montar HTML com dados externos: usar textContent, ou a renderização dos frameworks, que escapa o conteúdo. A segunda camada é a política de segurança de conteúdo (CSP), um cabeçalho HTTP que diz ao navegador de onde scripts, estilos e imagens podem vir. Com script-src 'self', mesmo que um atacante consiga injetar uma tag de script, o navegador se recusa a executar código inline ou de outro domínio.
Poluição de protótipo. Funções que mesclam objetos de forma recursiva, ao receber um JSON como {"__proto__": {"admin": true}}, podem acabar alterando o Object.prototype, e de repente todo objeto do processo "tem" a propriedade admin. A defesa é validar a entrada com um esquema, recusar chaves como __proto__, constructor e prototype ao mesclar, e usar Map ou Object.create(null) para dicionários com chaves vindas de fora.
Expressões regulares catastróficas (ReDoS). Algumas expressões, como /^(a+)+$/, levam um tempo que cresce exponencialmente com certas entradas. Como o event loop tem uma thread só, uma única requisição maliciosa pode travar o servidor inteiro por segundos ou minutos. Evite quantificadores aninhados, limite o tamanho do texto antes de testar e prefira validações simples.
Execução de texto como código. eval, new Function e setTimeout com texto executam qualquer coisa que chegue até eles. Não há bom motivo para usá-los com dados externos, e a CSP pode bloqueá-los por completo.
Segredos no navegador. Todo código enviado ao navegador é público, mesmo minificado. Chaves de API, senhas e regras que precisam ser confiáveis ficam no servidor. Ferramentas como o Vite só expõem ao código do navegador as variáveis de ambiente com um prefixo explícito (VITE_), justamente para evitar vazamentos acidentais.
Cadeia de suprimentos. Pacotes do npm podem rodar scripts durante a instalação, e já houve casos de pacotes legítimos sequestrados e de pacotes falsos com nomes parecidos com os verdadeiros. Lockfile versionado, npm ci, revisão das dependências novas, npm audit periódico e poucas dependências diretas reduzem a exposição.
Desempenho na prática
O primeiro passo é sempre medir. No navegador, a aba Performance das ferramentas do desenvolvedor mostra o que a thread principal fez em cada milissegundo, e as métricas do Google para a experiência real (as Core Web Vitals) apontam o que o usuário sente: quanto demora para o conteúdo principal aparecer, quanto a página pula enquanto carrega e quanto demora para responder a um clique. No Node, performance.now() mede trechos de código e a opção --cpu-prof grava um perfil de CPU que as mesmas ferramentas abrem.
Os ganhos mais comuns raramente vêm de micro-otimizações, e sim de decisões maiores:
- Enviar menos JavaScript. Cada quilobyte precisa ser baixado, analisado e compilado, e em celulares modestos isso pesa. Dividir o código por tela, remover dependências grandes usadas para pouco e não enviar ao navegador o que pode ser feito no servidor.
- Renderizar no servidor quando faz sentido. Páginas de conteúdo, como as de um blog ou de um site institucional, podem chegar prontas em HTML e quase não precisar de JavaScript. Aplicações muito interativas, como um painel de gestão, se beneficiam de rodar no navegador.
- Não fazer trabalho repetido. Uma busca que consulta a API a cada tecla deve esperar a pessoa parar de digitar por alguns milissegundos (a técnica chamada debounce). Resultados caros podem ficar em cache.
- Não bloquear a thread. No servidor, nada de leitura de arquivo síncrona (
readFileSync) no caminho de uma requisição; no navegador, listas com milhares de linhas devem renderizar só o que está visível.
Testes
Testes automatizados são especialmente valiosos numa linguagem dinâmica, em que muitos erros só aparecem quando o código roda. O Node traz um executor de testes embutido, o node:test, que dispensa dependências para projetos simples:
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { somaEmCentavos } from './valores.js';
test('soma valores sem erro de ponto flutuante', () => {
assert.equal(somaEmCentavos([0.1, 0.2]), 0.3);
});
Para projetos maiores, o Jest e o Vitest oferecem simulação de módulos (mocks), relatórios de cobertura e modo de observação; o Vitest compartilha a configuração do Vite, o que simplifica projetos que já o usam. Para fluxos que dependem de um navegador real, como entrar no sistema e emitir um pedido, o Playwright controla Chrome, Firefox e WebKit. Uma boa distribuição de esforço é ter muitos testes rápidos das regras de negócio (cálculos, conversões, datas, permissões), alguns testes de integração das rotas da API e poucos testes de ponta a ponta dos fluxos mais importantes.
Erros comuns
- **Usar
==** e ser surpreendido pela coerção. Use===. - Calcular dinheiro com ponto flutuante. Trabalhe com centavos inteiros e formate na exibição.
- **Esquecer o
await** ou usarasyncdentro deforEachesperando que ele aguarde. - **Perder o
this** ao passar um método como callback. Use arrow function oubind. - Mutar objetos e arrays compartilhados, como o estado de um componente React. Crie cópias com espalhamento,
map,filteroutoSorted. - Bloquear o event loop com processamento pesado no servidor. Use workers ou mova o trabalho para uma fila.
- Confiar em dados externos porque "o tipo diz que é assim". Valide na fronteira do sistema.
- Montar HTML com dados do usuário usando
innerHTML. Isso abre a porta para injeção de scripts (XSS). UsetextContentou a renderização do framework, que escapa o conteúdo. - Ignorar rejeições de Promise, que no Node derrubam o processo e no navegador somem no console.
Boas práticas
constpor padrão,letquando necessário, nuncavar.- Funções pequenas, com um propósito, e dados tratados de forma consistente.
- TypeScript em projetos que vão crescer, com o modo estrito ligado.
- ES Modules em código novo.
- Lint e formatação automáticos, rodando no editor e na integração contínua.
- Testes para regras de negócio, especialmente cálculos, datas e conversões.
- Lockfile versionado e
npm cino build. - Tratamento de erros em toda fronteira assíncrona, com logs que ajudem a investigar.
Como usamos JavaScript e TypeScript na Gothor
Na Gothor, o JavaScript, quase sempre na forma de TypeScript, está tanto nas interfaces quanto nos serviços. Usar a mesma linguagem nas duas pontas deixa a equipe circular entre elas com as mesmas ferramentas e convenções, e a verificação automática de tipos e de testes a cada mudança pega boa parte dos erros antes que eles cheguem a quem usa os sistemas. Os cuidados deste artigo, como tratar valores em dinheiro sem erro de ponto flutuante, cancelar o trabalho que ninguém mais espera e nunca inserir texto de fora numa página sem escapá-lo, fazem parte do nosso padrão.
Resumo
- JavaScript é dinâmico, de tipagem fraca e multiparadigma; ECMAScript é o padrão, e o TC39 publica uma edição por ano.
- A linguagem é a mesma em todo lugar; motores (V8, SpiderMonkey, JavaScriptCore) a executam, e ambientes (navegador, Node.js, Deno, Bun) fornecem o resto.
- Os motores compilam o código em tempo de execução e otimizam o que é previsível; vazamentos de memória são referências esquecidas.
- Use
===, cuidado com ponto flutuante, prefiraconstelet, e lembre quethisdepende de como a função é chamada. - Herança é delegação pela cadeia de protótipos;
classé uma sintaxe mais clara para isso. - O event loop executa o síncrono, esvazia as microtarefas e pega uma tarefa por vez; async/await deixa o código assíncrono legível, e código pesado deve ir para workers.
- Dinheiro em centavos inteiros, datas em UTC com o fuso só na exibição, textos normalizados antes de comparar.
- Erros são objetos
Errorcomcause; cancelamento e prazo vêm doAbortController; dados que chegam aos pedaços, das streams. - Módulos e classes já rodam em modo estrito;
usinglibera recursos no fim do bloco; workers trazem paralelismo de verdade, com mensagens copiadas ou memória compartilhada em páginas isoladas. - ES Modules são o padrão para código novo, e TypeScript traz tipos que existem só antes da execução; a validação na fronteira fecha o ciclo.
- XSS, poluição de protótipo, ReDoS e dependências comprometidas têm defesas conhecidas: escapar, validar, CSP e lockfile.
Para ir além
- JavaScript na MDN, a referência mais completa da linguagem, em português.
- O event loop, herança e cadeia de protótipos e módulos, na MDN.
- Especificação ECMAScript, as propostas em andamento no TC39, as propostas concluídas, por edição, e o documento de processo do TC39.
- Modo estrito, comparações de igualdade, Proxy, using, Temporal e SharedArrayBuffer, na MDN.
- O calendário de versões do Node.js.
- O event loop, os temporizadores e o nextTick no Node.js e a documentação de ES Modules do Node.
- Event loops na especificação HTML, para quem quer o detalhe do navegador.
- Documentação do TypeScript e do Zod.
- Intl, AbortController e Streams API, na MDN.
- Content Security Policy, na MDN, e o executor de testes do Node.
- React, Vite, NestJS, Deno e Bun.
Este post foi escrito com a ajuda da Juniper, a assistente de IA da Gothor, e revisado pela nossa equipe antes de ir ao ar.


