Ya sabes trabajar con datos, asincronía y errores. Lo que falta para moverte con soltura en cualquier proyecto es conocer las herramientas que vienen con el lenguaje y que aparecen una y otra vez. Las expresiones regulares son una de ellas: un patrón que describes una vez y aplicas a cualquier texto.
¿Qué es una expresión regular?#
Una expresión regular (o regex, del inglés regular expression) es una forma de describir un patrón de texto. En lugar de decir “este string concreto”, dices “un string que empieza por OW-, luego lleva letras mayúsculas, un guión y exactamente tres dígitos”. Cualquier string que encaje con esa descripción pasa la validación.
En JavaScript, una regex se escribe entre barras:
// el patrón entre barras: un literal es literal, un metacarácter describe una clase
/^OW-[A-Z]+-\d{3}$/No tienes que memorizar los metacaracteres: aprenderás a leerlos poco a poco y consultas la referencia cuando la necesitas. Lo que sí importa hoy es entender la idea: describes el molde una vez y el motor comprueba si un texto encaja.
Los metacaracteres básicos#
Cuando escribes /OW-DPS-017/ en una regex, cada letra se interpreta al pie de la letra. Pero en
cuanto usas metacaracteres, ya no describes un carácter concreto sino una clase de caracteres:
// \d → cualquier dígito del 0 al 9
// \w → cualquier letra, dígito o guión bajo
// . → cualquier carácter (excepto salto de línea)
// [A-Z] → cualquier letra mayúscula
// [a-z] → cualquier letra minúscula
// + → uno o más del elemento anterior
// ? → cero o uno (el elemento anterior es opcional)
// {3} → exactamente 3 repeticiones del elemento anterior
// ^ → inicio del string
// $ → fin del stringNo intentes memorizar esto ahora: es una referencia a la que vuelves. Hoy basta con saber que existen
y aplicar test y match con los ejemplos de abajo.
Las flags: g e i#
Las flags modifican el comportamiento de la regex y se escriben después de la segunda barra:
// i = case-insensitive: ignora si las letras son mayúsculas o minúsculas
/tracer/i // casa con "tracer", "Tracer", "TRACER"...
// g = global: actúa sobre TODAS las coincidencias, no solo la primera
/tracer/g // en replace y matchAll, reemplaza o recorre todas las apariciones
// se combinan: primero g, luego i (o al revés: el orden no importa)
/tracer/giregex.test(texto): ¿encaja sí o no?#
test es la pregunta de sí/no: devuelve un booleano según si el patrón aparece en el texto. Es
lo que usas para validar:
// el patrón de un id válido
const patronId = /^OW-[A-Z]+-\d{3}$/;
// ^ fuerza que el patrón empiece al inicio del string
// [A-Z]+ uno o más letras mayúsculas (el rol)
// \d{3} exactamente tres dígitos
// $ fuerza que el patrón termine al final del string
// true: encaja del principio al final
console.log(patronId.test("OW-DPS-017"));
// false: minúsculas y un solo dígito
console.log(patronId.test("ow-tank-9"));texto.match(regex): extraer lo que casa#
match va más allá de sí/no: devuelve la parte del texto que encajó. Si el patrón no casa, devuelve
null (no un array vacío). Comprueba siempre el resultado antes de usarlo:
const id = "OW-DPS-017";
// match devuelve un array: el primer elemento es lo que casó en total
const resultado = id.match(/^OW-[A-Z]+-\d{3}$/);
// si no casa, resultado es null
if (resultado) {
// "OW-DPS-017" (el match completo)
console.log(resultado[0]);
}Con grupos nombrados ((?<nombre>...)), match también te da las partes capturadas en
match.groups:
// (?<rol>...) etiqueta el grupo "letras mayúsculas" como "rol"
// (?<num>...) etiqueta el grupo "tres dígitos" como "num"
const m = "OW-DPS-017".match(/^OW-(?<rol>[A-Z]+)-(?<num>\d{3})$/);
if (m) {
// "DPS": lo que capturó el grupo nombrado "rol"
console.log(m.groups.rol);
// "017": lo que capturó el grupo nombrado "num"
console.log(m.groups.num);
}Los grupos nombrados evitan depender de posiciones numéricas (m[1], m[2]): el código se lee
solo. Cuando la regex cambia, los nombres siguen siendo válidos.
¿Qué pasa cuando no casa?
// null, no un array vacío
const malo = "ow-tank-9".match(/^OW-[A-Z]+-\d{3}$/);
// null
console.log(malo);texto.replace(regex, reemplazo): cambiar lo que casa#
replace sustituye lo que encaja con el patrón. Sin flag g, solo cambia la primera aparición;
con g, todas. Con i, sin importar mayúsculas:
// Sin flag g, replace solo cambia la PRIMERA coincidencia. Con g, todas.
const frase = "tracer y TRACER juegan";
// g = todas las apariciones, i = ignora mayúsculas
// "Tracer y Tracer juegan"
console.log(frase.replace(/tracer/gi, "Tracer"));texto.matchAll(regex): todas las coincidencias#
matchAll devuelve un iterador con todas las coincidencias. Necesita la flag g; sin ella,
lanza un error. Como devuelve un iterador y no un array, hay que desplegarlo con [...] o
Array.from para recorrerlo o mapearlo:
// La flag g es OBLIGATORIA con matchAll
const texto = "picks: Genji, Mercy, Ana";
// [A-Z][a-z]+ → una mayúscula seguida de minúsculas (un nombre propio)
// matchAll devuelve un iterador: hay que desplegarlo con [...] antes de map
const nombres = [...texto.matchAll(/[A-Z][a-z]+/g)].map((x) => x[0]);
// ["Genji", "Mercy", "Ana"]
console.log(nombres);matchAll con grupos nombrados es especialmente útil: cada coincidencia lleva su .groups, igual
que en match. Así puedes recorrer un texto entero extrayendo partes estructuradas de cada aparición.
Pruébalo tú#
Mira la consola: la regex valida el id, los grupos nombrados extraen rol y número, replace corrige
el nombre en todo el texto, y matchAll recoge todos los nombres propios. Cambia el id en la línea
de test, prueba un id inválido, o añade un nombre nuevo al texto de matchAll. Pulsa Ejecutar (o Ctrl+Enter) para ver los resultados.
Comprueba lo que sabes#
Pregunta 1 de 5
¿Qué devuelve `regex.test(texto)`?
Tu turno#
Valida los ids del roster. Uno viene mal escrito: márcalo. En el nivel Excelente, usa grupos nombrados
para extraer el rol y el número y muéstralos en la tabla. Cuando lo tengas, despliega las soluciones y
observa cómo el nivel Excelente encapsula la regex en una función pura y usa matchAll para recorrer
todos los ids de una sola pasada.
Ejercicio · en esta página
Valida y extrae los ids del roster
Recibes el roster en bruto: solo ids y nombres (uno de los ids está mal escrito). Monta una tabla que valide cada id y, en el nivel Excelente, extraiga el rol y el número para mostrarlos junto al id.
Paso 1: Que funcione
- Muestra una fila por héroe con su id, nombre y estado.
- Marcas el id que no cumple el patrón.
- Vale validar el id a mano con comprobaciones de string (sin regex).
Paso 2: Que esté pulido
- Usas una expresión regular con test() para validar el id en una línea.
- La regex cubre el patrón completo: OW-, letras mayúsculas, guión, tres dígitos.
- El código de validación es declarativo y se lee de un vistazo.
Paso 3: Que sea excelente
- Usas grupos nombrados en la regex para extraer rol y número del id válido.
- Una función pura analizarId() encapsula la regex y devuelve un objeto limpio.
- Usas matchAll con la flag g para recorrer todas las coincidencias del roster.
Ver soluciones
// ════════════════════════════════════════════════════════════════════════════
// NIVEL OK — "funciona"
//
// Valida el id a mano: comprueba que empiece por "OW-", que tenga la longitud
// esperada y que el último trozo sean tres caracteres. Funciona, pero...
// - Las comprobaciones de string sueltas son verbosas y fáciles de equivocar:
// basta un caso que no hayas previsto (id con guión extra, letras con tilde)
// para que un id inválido pase o uno válido falle.
// - No extrae nada del id: si luego necesitas el rol o el número, tienes que
// escribir más código de corte de strings encima de esto.
// - Es justo el trabajo que una expresión regular ya hace por ti en una línea.
// ════════════════════════════════════════════════════════════════════════════
// Copia local del roster para que esta solución sea autocontenida.
const EQUIPO = [
{ id: "OW-DPS-017", nombre: "Tracer" },
{ id: "OW-SUP-004", nombre: "Mercy" },
{ id: "ow-tank-9", nombre: "Reinhardt" },
{ id: "OW-DPS-048", nombre: "Genji" },
{ id: "OW-SUP-021", nombre: "Ana" },
];
// Comprueba si el id es válido a mano, sin regex.
// Un id válido: "OW-", un bloque de letras, "-", exactamente tres dígitos.
function esIdValido(id) {
// si no empieza por "OW-", descartamos
if (id.slice(0, 3) !== "OW-") return false;
// separamos en trozos por "-"
var trozos = id.split("-");
// debe tener exactamente tres trozos: ["OW", "<ROL>", "<NUM>"]
if (trozos.length !== 3) return false;
// el primer trozo debe ser "OW"
if (trozos[0] !== "OW") return false;
// el segundo trozo debe ser al menos una letra (comprobamos solo que no esté vacío)
if (trozos[1].length === 0) return false;
// el tercer trozo debe ser exactamente 3 caracteres numéricos
if (trozos[2].length !== 3) return false;
// comprobamos que los tres caracteres sean dígitos del 0 al 9
var num = trozos[2];
if (num[0] < "0" || num[0] > "9") return false;
if (num[1] < "0" || num[1] > "9") return false;
if (num[2] < "0" || num[2] > "9") return false;
return true;
}
var lineas = EQUIPO.map(function (heroe) {
// comprobamos el id con la función manual
var valido = esIdValido(heroe.id);
// el estado dice "Válido" o avisa del problema
var estado = valido ? "Válido" : "Id no válido";
return heroe.id + " | " + heroe.nombre + " | " + estado;
});
function mostrar(lineas) {
console.log("=== Roster del equipo ===");
for (var i = 0; i < lineas.length; i++) {
console.log(lineas[i]);
}
}
mostrar(lineas); Por qué este nivel
- Validar el id a mano con comprobaciones de string sueltas (longitud, rangos de caracteres, guiones) es verboso y fácil de equivocar: basta un caso no previsto para que un id inválido pase o uno válido falle.
- No extrae nada del id: si luego necesitas el rol o el número, hay que escribir más código de corte de strings encima de esto.
- Es justo el trabajo que una expresión regular ya hace por ti en una línea.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL MEJOR — "pulido"
//
// Por qué mejora a OK:
// - Una sola regex reemplaza todas las comprobaciones manuales de string.
// /^OW-[A-Z]+-\d{3}$/.test(id) es declarativo y se lee de un vistazo:
// "empieza por OW-, letras mayúsculas, guión, tres dígitos, fin."
// - Es mucho más difícil de equivocar: la regex cubre todos los casos que
// las comprobaciones sueltas podían olvidar (p. ej. letras minúsculas en
// el rol, número de dígitos distinto de tres).
// - El código de validación pasa de ~12 líneas a 1.
//
// Su límite respecto a Excelente: test() devuelve true/false, no extrae nada.
// Si necesitas el rol o el número del id, tienes que hacer un match aparte.
// ════════════════════════════════════════════════════════════════════════════
// Copia local del roster para que esta solución sea autocontenida.
const EQUIPO = [
{ id: "OW-DPS-017", nombre: "Tracer" },
{ id: "OW-SUP-004", nombre: "Mercy" },
{ id: "ow-tank-9", nombre: "Reinhardt" },
{ id: "OW-DPS-048", nombre: "Genji" },
{ id: "OW-SUP-021", nombre: "Ana" },
];
// Una regex declarativa reemplaza todas las comprobaciones manuales:
// ^ inicio del string
// OW- literal "OW-"
// [A-Z]+ una o más letras mayúsculas (el rol)
// - literal "-"
// \d{3} exactamente tres dígitos
// $ fin del string
const PATRON_ID = /^OW-[A-Z]+-\d{3}$/;
var lineas = EQUIPO.map(function (heroe) {
// test devuelve true si el id encaja con el patrón, false si no
var valido = PATRON_ID.test(heroe.id);
// el estado refleja el resultado
var estado = valido ? "Válido" : "Id no válido";
return heroe.id + " | " + heroe.nombre + " | " + estado;
});
function mostrar(lineas) {
console.log("=== Roster del equipo ===");
for (var i = 0; i < lineas.length; i++) {
console.log(lineas[i]);
}
}
mostrar(lineas); Por qué es mejor que el anterior
- Una sola regex reemplaza todas las comprobaciones manuales: /^OW-[A-Z]+-\d{3}$/.test(id) es declarativo y se lee de un vistazo.
- Es mucho más difícil de equivocar: la regex cubre todos los casos que las comprobaciones sueltas podían olvidar.
- Su límite: test() devuelve true/false, no extrae nada. Si necesitas el rol o el número, hay que hacer un match aparte.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL EXCELENTE — "óptimo"
//
// Por qué mejora a Mejor:
// - La regex usa GRUPOS NOMBRADOS: /^OW-(?<rol>[A-Z]+)-(?<num>\d{3})$/.
// Con match() en vez de test(), no solo valida: EXTRAE rol y número en
// una sola operación. match().groups.rol da "DPS", match().groups.num da "017".
// - La función analizarId() encapsula esa lógica y devuelve un objeto limpio
// { valido: false } | { valido: true, rol, num }. El llamador no necesita
// saber nada sobre la regex: solo pregunta qué contiene el resultado.
// - matchAll() recorre TODOS los ids válidos del roster de una vez y extrae
// solo sus roles, mostrando cómo iterar sobre múltiples coincidencias.
// - Funciones puras (entran datos, salen datos, sin tocar el DOM): el cálculo
// se prueba aparte y la presentación solo recibe el resultado ya montado.
// ════════════════════════════════════════════════════════════════════════════
// Copia local del roster para que esta solución sea autocontenida.
const EQUIPO = [
{ id: "OW-DPS-017", nombre: "Tracer" },
{ id: "OW-SUP-004", nombre: "Mercy" },
{ id: "ow-tank-9", nombre: "Reinhardt" },
{ id: "OW-DPS-048", nombre: "Genji" },
{ id: "OW-SUP-021", nombre: "Ana" },
];
// Grupos nombrados: (?<rol>…) y (?<num>…) etiquetan lo que capturan.
// match() devuelve null si no casa; si casa, .groups trae las partes con nombre.
const PATRON_ID = /^OW-(?<rol>[A-Z]+)-(?<num>\d{3})$/;
// Función pura: dado un id, devuelve { valido: false } o { valido: true, rol, num }.
// Nadie de fuera necesita saber que por dentro hay una regex.
function analizarId(id) {
// null si no casa
var m = id.match(PATRON_ID);
// id inválido: devolvemos un objeto con valido false
if (!m) return { valido: false };
// id válido: devolvemos las partes extraídas por los grupos nombrados
return { valido: true, rol: m.groups.rol, num: m.groups.num };
}
var lineas = EQUIPO.map(function (heroe) {
// analizarId da el resultado ya listo: sin necesidad de saber regex aquí
var resultado = analizarId(heroe.id);
// si es válido, mostramos rol y número extraídos; si no, avisamos
var estado = resultado.valido
? "Válido — " + resultado.rol + " #" + resultado.num
: "Id no válido";
return heroe.id + " | " + heroe.nombre + " | " + estado;
});
function mostrar(lineas) {
console.log("=== Roster del equipo ===");
for (var i = 0; i < lineas.length; i++) {
console.log(lineas[i]);
}
}
mostrar(lineas);
// ── Bonus: matchAll para iterar TODAS las coincidencias de una vez ──────────
// Unimos todos los ids en un string y extraemos los que son válidos con matchAll.
// matchAll devuelve un iterador: hay que desplegarlo con [...] para recorrerlo.
var todosLosIds = EQUIPO.map(function (h) {
return h.id;
}).join(" | ");
// la flag g es obligatoria con matchAll
var patron_g = /OW-(?<rol>[A-Z]+)-(?<num>\d{3})/g;
// desplegamos el iterador a array y mapeamos solo el rol de cada coincidencia
var rolesEncontrados = Array.from(todosLosIds.matchAll(patron_g)).map(
function (m) {
return m.groups.rol;
},
);
// ["DPS", "SUP", "DPS", "SUP"] — solo los ids válidos
console.log("Roles válidos encontrados: " + rolesEncontrados.join(", ")); Por qué es mejor que el anterior
- Grupos nombrados en la regex: no solo valida, EXTRAE rol y número en una sola operación. match().groups.rol da 'DPS', match().groups.num da '017'.
- analizarId() encapsula la regex y devuelve un objeto limpio: el llamador no necesita saber nada sobre patrones para usar el resultado.
- matchAll recorre TODOS los ids válidos del roster de una vez, mostrando cómo iterar sobre múltiples coincidencias. El bonus al final del fichero ilustra el patrón completo.