Regex em JavaScript: expressões regulares na prática
Literal e new RegExp, grupos nomeados, matchAll e a armadilha da flag global que faz o test alternar entre true e false a cada chamada — tudo executado.
Expressão regular é um padrão de texto escrito numa linguagem própria. Em
JavaScript ela é um objeto de primeira classe: você escreve entre barras
(/\d{5}-?\d{3}/), guarda numa variável e usa nos métodos de String e de
RegExp.
A resposta curta: regex.test(texto) devolve verdadeiro ou falso,
texto.match(regex) devolve o que casou, texto.matchAll(regex) devolve todas
as ocorrências e texto.replace(regex, novo) troca. E a armadilha número um:
regex com a flag g guarda estado entre chamadas — usar a mesma constante
duas vezes num test devolve resultados diferentes. O resto deste texto mostra
cada caso rodando.
Os exemplos vêm de uma loja: CEP, cupom, SKU, preço em real, lista de pedidos.
Um gabarito transparente sobre o texto
Pense num gabarito transparente colocado sobre uma ficha de pedido. Os recortes mostram onde pode haver número, hífen ou letra; o que não se alinha fica de fora. O gabarito não entende o significado de CEP ou cupom — apenas reconhece o formato que você desenhou.
Uma expressão regular é esse padrão de correspondência aplicado a uma
sequência de caracteres. Métodos diferentes perguntam se houve casamento,
recuperam trechos ou substituem o que casou. Na primeira microprática, marque no
texto os caracteres que o padrão deveria cobrir antes de executar; o resultado
de test ou match confirma se o gabarito foi desenhado como você imaginou.
Existem duas formas de criar. A literal, entre barras, é compilada uma vez
quando o arquivo é lido. A construída, com new RegExp, recebe uma string —
e é aí que mora a pegadinha do escape:
const cepLiteral = /^\d{5}-?\d{3}$/;
const cepConstruido = new RegExp('^\\d{5}-?\\d{3}$');
console.log('literal :', cepLiteral.test('01310-100'));
console.log('construído:', cepConstruido.test('01310100'));
console.log('inválido :', cepLiteral.test('0131'));
console.log('source iguais:', cepLiteral.source === cepConstruido.source);
const errado = new RegExp('^\d{5}$');
console.log('escape simples:', errado.source, '->', errado.test('01310'));A última linha é o motivo de todo mundo preferir o literal. Dentro de uma
string, '\d' não é “dígito”: a barra invertida é consumida pela própria string
e sobra a letra d. O padrão virou ^d{5}$ — cinco letras “d” —, e é por isso
que ele recusou 01310 sem dar erro nenhum. Para produzir \d numa string você
precisa escrever '\\d', com duas barras.
Regra prática: literal por padrão; new RegExp só quando parte do padrão vem
de variável.
O que cada método devolve
test devolve booleano. match devolve um array com informação extra
pendurada: o texto casado na posição 0, os grupos capturados nas posições
seguintes, e as propriedades index e groups.
const linha = 'Pedido 4821 — 2026-03-14 — R$ 1.289,90 — SP';
const data = /(?<ano>\d{4})-(?<mes>\d{2})-(?<dia>\d{2})/;
const achado = linha.match(data);
console.log('match completo:', achado[0]);
console.log('grupos :', achado.groups);
const { ano, mes, dia } = achado.groups;
console.log('formatado :', `${dia}/${mes}/${ano}`);
console.log('índice :', achado.index);(?<nome>...) é um grupo nomeado. Ele custa quatro caracteres a mais e paga
na leitura: achado.groups.mes diz o que é, enquanto achado[2] obriga quem lê
a contar parênteses. Quando a regex muda e você insere um grupo no meio, os
índices se deslocam e o código quebra em silêncio; os nomes, não.
O [Object: null prototype] na saída não é bug: groups é criado sem
protótipo, justamente para que um grupo chamado constructor ou toString não
colida com nada.
A flag global faz o test alternar
Este é o comportamento que mais confunde, e ele é fácil de reproduzir. Mesma regex, mesmo texto, quatro chamadas seguidas:
const cupom = /CLUB\d{2}/g;
const texto = 'CLUB10';
console.log(cupom.test(texto), cupom.lastIndex);
console.log(cupom.test(texto), cupom.lastIndex);
console.log(cupom.test(texto), cupom.lastIndex);
console.log(cupom.test(texto), cupom.lastIndex);Verdadeiro, falso, verdadeiro, falso. Com a flag g, o objeto regex é
mutável: depois de casar, ele grava em lastIndex a posição onde parou (6) e
a próxima busca começa dali. Como não há mais nada depois do caractere 6, a
segunda chamada falha — e, ao falhar, zera lastIndex. Daí o pêndulo.
Sem a flag g, lastIndex nunca sai de zero e o resultado é estável:
const cupom = /CLUB\d{2}/;
const texto = 'CLUB10';
for (let i = 0; i < 4; i++) console.log(cupom.test(texto), cupom.lastIndex);Quando você precisa mesmo do g para varrer o texto, o lastIndex deixa de ser
armadilha e vira ferramenta — é ele que permite ao exec continuar de onde
parou:
const bruto = 'teclado ; mouse,mousepad; headset';
console.log(bruto.split(/\s*[;,]\s*/));
const skus = 'TEC-001 MOU-014 PAD-007';
const re = /(?<familia>[A-Z]{3})-(?<numero>\d{3})/g;
let m;
while ((m = re.exec(skus)) !== null) {
console.log(m.groups.familia, m.groups.numero, '| lastIndex:', re.lastIndex);
}matchAll: o laço sem estado na mão
O exec num while funciona, mas é fácil esquecer o g e criar um laço
infinito. Desde 2020 existe matchAll, que devolve um iterador de resultados
completos — cada um com groups e index:
const pedidos = `
4821 | Teclado mecânico | R$ 289,90
4822 | Mouse sem fio | R$ 149,90
4823 | Mousepad XL | R$ 79,90
`;
const linha = /^(?<id>\d{4}) \| (?<produto>.+?) +\| R\$ +(?<preco>[\d.,]+)$/gm;
for (const m of pedidos.matchAll(linha)) {
const { id, produto, preco } = m.groups;
console.log(id, '->', produto.padEnd(18), preco);
}
const total = [...pedidos.matchAll(linha)]
.reduce((soma, m) => soma + Number(m.groups.preco.replace('.', '').replace(',', '.')), 0);
console.log('total:', total.toFixed(2));matchAll exige a flag g. Sem ela, o motor recusa em vez de devolver um
resultado meia-boca:
const texto = 'CLUB10 CLUB25';
console.log([...texto.matchAll(/CLUB\d{2}/)]);Compare com match sem g, que devolve só a primeira ocorrência sem reclamar.
A diferença é deliberada: matchAll promete “todas”, e uma regex sem g não
tem como cumprir.
replace, replaceAll e a flag obrigatória
replace com uma regex sem g troca só a primeira ocorrência. Com g,
troca todas. replaceAll com uma string troca todas sempre:
const sku = 'TEC-MEC-RGB-BR';
console.log('replace sem g :', sku.replace(/-/, '/'));
console.log('replace com g :', sku.replace(/-/g, '/'));
console.log('replaceAll texto:', sku.replaceAll('-', '/'));Agora a parte que pega todo mundo. replaceAll também aceita regex, mas
recusa uma regex sem g — e não em silêncio:
const descricao = 'Teclado mecânico RGB, cor preta, RGB retroiluminado';
console.log(descricao.replaceAll('RGB', 'com LED'));
console.log(descricao.replaceAll(/rgb/gi, 'com LED'));
console.log(descricao.replaceAll(/rgb/i, 'com LED'));As duas primeiras linhas funcionaram; a terceira derrubou o processo. A regra é
literal: se o argumento é regex, ela precisa ter g, senão o nome do método
seria mentira. Esse TypeError é um dos poucos casos em que a linguagem prefere
gritar a adivinhar — e vale entender por que, no panorama dos
tipos de erro em JavaScript.
No texto de substituição você pode referenciar os grupos: $1, $2 por posição
ou $<nome> por nome. E, se a troca precisar de cálculo, o segundo argumento
aceita uma função:
const texto = 'Entrega prevista para 2026-03-14 e retorno em 2026-04-02.';
const iso = /(?<ano>\d{4})-(?<mes>\d{2})-(?<dia>\d{2})/g;
console.log(texto.replace(iso, '$<dia>/$<mes>/$<ano>'));
const precos = 'Teclado R$ 289,90 e Mouse R$ 149,90';
console.log(precos.replace(/R\$ (\d+),(\d{2})/g, (_, reais, centavos) =>
`R$ ${(Number(reais) * 1.1).toFixed(0)},${centavos}`));Guloso contra preguiçoso
Por padrão os quantificadores *, + e {n,} são gulosos: pegam o máximo
que conseguem e só devolvem caractere se o resto do padrão não fechar. Um ?
depois deles inverte o comportamento:
const html = '<b>Teclado mecânico</b> por <b>R$ 289,90</b>';
console.log('guloso :', html.match(/<b>.*<\/b>/)[0]);
console.log('preguiço:', html.match(/<b>.*?<\/b>/)[0]);
console.log('todos :', html.match(/<b>.*?<\/b>/g));O guloso engoliu o </b> do meio e foi até o último. Nove em cada dez vezes que
uma regex “pega demais”, é isso — e o ? resolve.
As flags que valem a pena decorar
| flag | efeito | exemplo de uso |
|---|---|---|
g |
percorre todas as ocorrências e usa lastIndex |
matchAll, replaceAll, replace global |
i |
ignora maiúscula e minúscula | cupom digitado pelo cliente |
m |
^ e $ passam a valer por linha |
varrer um bloco de texto colado |
s |
o . passa a casar quebra de linha |
descrição de produto em várias linhas |
u |
ativa Unicode e libera \p{...} |
aceitar acento em nome de cliente |
Todas em ação:
const nota = `Cliente: Ana Souza
Cupom: club10
Observação: entregar
até sexta`;
console.log('i :', /CLUB10/i.test(nota));
console.log('m :', nota.match(/^Cupom: (\S+)$/m)[1]);
console.log('s :', /entregar.até/s.test(nota));
console.log('sem s:', /entregar.até/.test(nota));
console.log('u :', /^\p{L}+$/u.test('mecânico'));O par s / sem s é a demonstração mais útil: por padrão, o ponto casa
qualquer caractere menos a quebra de linha. Já \p{L} com a flag u
significa “qualquer letra em qualquer alfabeto” — e é a forma correta de aceitar
“mecânico”, “José” e “Gonçalves” sem listar acento por acento.
Validar os campos de um checkout
O uso mais comum de regex em front-end é o primeiro filtro de formulário. As quatro regras abaixo cobrem o que um checkout brasileiro pede, aceitando o campo com ou sem máscara:
const validadores = {
cep: /^\d{5}-?\d{3}$/,
cpf: /^\d{3}\.?\d{3}\.?\d{3}-?\d{2}$/,
telefone: /^\(?(\d{2})\)?\s?9?\d{4}-?\d{4}$/,
cartao: /^\d{4} ?\d{4} ?\d{4} ?\d{4}$/,
};
const entradas = {
cep: ['01310-100', '01310100', '1310-100'],
cpf: ['123.456.789-09', '12345678909', '123.456.789'],
telefone: ['(11) 98765-4321', '11987654321', '9876-5432'],
cartao: ['4111 1111 1111 1111', '4111111111111111', '4111 1111'],
};
for (const [campo, regra] of Object.entries(validadores)) {
for (const valor of entradas[campo]) {
console.log(campo.padEnd(9), valor.padEnd(20), regra.test(valor));
}
}Nenhuma delas é validação de verdade. A regex confirma o formato; o dígito verificador do CPF e o algoritmo de Luhn do cartão são conta, não padrão de texto. Trate a regex como o porteiro que barra o obviamente errado antes de gastar uma requisição.
Para regras compostas — senha com maiúscula, minúscula, número e símbolo — o
(?=...), chamado de lookahead, testa sem consumir caractere:
const forte = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{10,}$/;
for (const senha of ['clubstore', 'ClubStore1', 'ClubStore1!', 'Cs1!']) {
console.log(senha.padEnd(14), forte.test(senha));
}Cada (?=...) é uma condição independente, verificada a partir do início. O
.{10,}$ no fim é o que exige o tamanho.
Regex montada com texto do usuário
Quando o padrão inclui algo digitado — a busca de produtos, por exemplo — o
new RegExp deixa de ser conveniência e vira risco. Um parêntese solto na caixa
de busca derruba a página:
const produtos = ['Teclado mecânico', 'Mouse sem fio (RGB)', 'Cabo USB-C 2m'];
function buscarIngenuo(termo) {
const re = new RegExp(termo, 'i');
return produtos.filter((p) => re.test(p));
}
function escapar(texto) {
return texto.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function buscarSeguro(termo) {
const re = new RegExp(escapar(termo), 'i');
return produtos.filter((p) => re.test(p));
}
console.log('seguro :', buscarSeguro('(RGB)'));
try {
console.log('ingênuo :', buscarIngenuo('(RGB'));
} catch (erro) {
console.log(erro.name, '|', erro.message);
}A função escapar transforma cada metacaractere em literal — o $& no
substituto significa “o que casou”. E o try/catch em volta é obrigatório
sempre que o padrão vem de fora: essa é uma das situações em que
capturar o erro não é preciosismo, é a diferença
entre um resultado vazio e uma tela branca.
Quando não usar regex
Regex é excelente para padrão de formato e péssima para estrutura aninhada. Quatro casos em que a resposta certa é outra ferramenta:
- HTML e XML. Aninhamento não cabe numa linguagem regular. Use
DOMParserno navegador. - JSON.
JSON.parsejá existe, é mais rápido e reclama do lugar certo. - Dinheiro e data.
Intl.NumberFormateIntl.DateTimeFormatformatam melhor do que qualquerreplaceque você escreva. - Uma substring fixa.
texto.includes('RGB')é mais legível e mais rápido que/RGB/.test(texto).
E duas regras de higiene: comente toda regex com mais de vinte caracteres com
uma linha em português dizendo o que ela aceita, e desconfie de aninhamento como
(a+)+ — ele abre a porta para o travamento por retrocesso catastrófico, em que
uma entrada de trinta caracteres congela a aba.
Com texto sob controle, o próximo passo natural é organizar essas funções em arquivos separados: veja módulos em JavaScript. O mapa completo da trilha de JavaScript e a ordem de estudo estão no guia completo de JavaScript.
Perguntas frequentes
Por que meu test alterna entre true e false?
Quando usar new RegExp em vez do literal?
Regex serve para validar e-mail?
Como debugo uma regex que não casa?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- MDN — Expressões regulares — developer.mozilla.org
- MDN — RegExp.prototype.lastIndex — developer.mozilla.org
- MDN — String.prototype.replaceAll — developer.mozilla.org


