Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIntermediáriocódigo testado

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.

Rodolfo Mori8 min de leitura

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:

js
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'));
literal : true construído: true inválido : false source iguais: true escape simples: ^d{5}$ -> false

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.

js
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);
match completo: 2026-03-14 grupos : [Object: null prototype] { ano: '2026', mes: '03', dia: '14' } formatado : 14/03/2026 índice : 14

(?<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:

js
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);
true 6 false 0 true 6 false 0

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:

js
const cupom = /CLUB\d{2}/;
const texto = 'CLUB10';

for (let i = 0; i < 4; i++) console.log(cupom.test(texto), cupom.lastIndex);
true 0 true 0 true 0 true 0

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:

js
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);
}
[ 'teclado', 'mouse', 'mousepad', 'headset' ] TEC 001 | lastIndex: 7 MOU 014 | lastIndex: 15 PAD 007 | lastIndex: 23

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:

js
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));
4821 -> Teclado mecânico 289,90 4822 -> Mouse sem fio 149,90 4823 -> Mousepad XL 79,90 total: 519.70

matchAll exige a flag g. Sem ela, o motor recusa em vez de devolver um resultado meia-boca:

js
const texto = 'CLUB10 CLUB25';
console.log([...texto.matchAll(/CLUB\d{2}/)]);
file:///private/tmp/loja/r09.mjs:2 console.log([...texto.matchAll(/CLUB\d{2}/)]); ^ TypeError: String.prototype.matchAll called with a non-global RegExp argument at String.matchAll (<anonymous>) at file:///private/tmp/loja/r09.mjs:2:23 at ModuleJob.run (node:internal/modules/esm/module_job:439:25) at async node:internal/modules/esm/loader:633:26 at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5) Node.js v24.16.0

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:

js
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('-', '/'));
replace sem g : TEC/MEC-RGB-BR replace com g : TEC/MEC/RGB/BR replaceAll texto: TEC/MEC/RGB/BR

Agora a parte que pega todo mundo. replaceAll também aceita regex, mas recusa uma regex sem g — e não em silêncio:

js
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'));
Teclado mecânico com LED, cor preta, com LED retroiluminado Teclado mecânico com LED, cor preta, com LED retroiluminado file:///private/tmp/loja/r07.mjs:5 console.log(descricao.replaceAll(/rgb/i, 'com LED')); ^ TypeError: String.prototype.replaceAll called with a non-global RegExp argument at String.replaceAll (<anonymous>) at file:///private/tmp/loja/r07.mjs:5:23 at ModuleJob.run (node:internal/modules/esm/module_job:439:25) at async node:internal/modules/esm/loader:633:26 at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5) Node.js v24.16.0

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:

js
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}`));
Entrega prevista para 14/03/2026 e retorno em 02/04/2026. Teclado R$ 318,90 e Mouse R$ 164,90

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:

js
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));
guloso : <b>Teclado mecânico</b> por <b>R$ 289,90</b> preguiço: <b>Teclado mecânico</b> todos : [ '<b>Teclado mecânico</b>', '<b>R$ 289,90</b>' ]

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:

js
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'));
i : true m : club10 s : true sem s: false u : true

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:

js
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));
  }
}
cep 01310-100 true cep 01310100 true cep 1310-100 false cpf 123.456.789-09 true cpf 12345678909 true cpf 123.456.789 false telefone (11) 98765-4321 true telefone 11987654321 true telefone 9876-5432 false cartao 4111 1111 1111 1111 true cartao 4111111111111111 true cartao 4111 1111 false

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:

js
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));
}
clubstore false ClubStore1 false ClubStore1! true Cs1! false

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:

js
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);
}
seguro : [ 'Mouse sem fio (RGB)' ] SyntaxError | Invalid regular expression: /(RGB/i: Unterminated group

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 DOMParser no navegador.
  • JSON. JSON.parse já existe, é mais rápido e reclama do lugar certo.
  • Dinheiro e data. Intl.NumberFormat e Intl.DateTimeFormat formatam melhor do que qualquer replace que 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.

  • regex
  • expressoes regulares
  • string
  • validacao
  • replaceall

Perguntas frequentes

Por que meu test alterna entre true e false?
Porque a regex tem a flag g e você guardou ela numa constante. Com g, o objeto lembra onde parou na propriedade lastIndex, e a chamada seguinte recomeça dali. Tire o g para validar, ou zere lastIndex antes de cada teste. Regex com g é para percorrer, não para validar.
Quando usar new RegExp em vez do literal?
Só quando o padrão é montado em tempo de execução, com um pedaço vindo de variável. Em todo o resto, o literal é melhor: o motor compila uma vez, o editor destaca a sintaxe e você escreve uma barra invertida em vez de duas.
Regex serve para validar e-mail?
Serve para descartar o obviamente errado — sem arroba, sem ponto depois da arroba. Não serve para garantir que o endereço existe. A regra oficial de e-mail é grande demais para caber numa regex legível; quem confirma o endereço é o e-mail de confirmação que você envia.
Como debugo uma regex que não casa?
Quebre em pedaços e teste cada um separado no console. Comece pela âncora (^ e $), depois um grupo por vez. Ferramentas como o regex101 mostram o casamento passo a passo, mas o hábito mais útil é imprimir o resultado de match e olhar o índice onde ele parou.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

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

  1. MDN — Expressões regulares — developer.mozilla.org
  2. MDN — RegExp.prototype.lastIndex — developer.mozilla.org
  3. MDN — String.prototype.replaceAll — developer.mozilla.org

Continue por aqui