Módulo 08: Cadenas y expresiones regulares
Objetivos del módulo
- Elegir entre comillas simples, dobles y backticks, y dominar los template literals
- Crear tagged templates y conocer
String.raw - Entender que las cadenas son inmutables y acceder a sus caracteres con
at,charAty[] - Usar con soltura los métodos de
Stringmás importantes - Comprender Unicode: code units, code points, emojis y grafemas (
Intl.Segmenter) - Detectar y reparar cadenas mal formadas con
isWellFormedytoWellFormed - Escribir expresiones regulares: clases, cuantificadores, anclas, grupos y lookarounds
- Usar los flags
g,i,m,s,u,v,dey - Aplicar
test,exec,match,matchAll,replaceysplitcon regex - Construir regex seguras a partir de texto del usuario con
RegExp.escape(ES2025) - Validar datos típicos: email, código postal, DNI y contraseñas
1. Crear cadenas
📘 Concepto: Una cadena (string) es una secuencia de caracteres de texto. En JavaScript se escribe entre comillas simples, dobles o acentos graves (backticks).
const simples = 'Hola';
const dobles = "Hola";
const backticks = `Hola`;
// Cada tipo de comilla permite usar los otros dentro sin escapar
const frase1 = 'Ella dijo "hola"';
const frase2 = "It's JavaScript";
const frase3 = `Usa 'simples' o "dobles" sin problema`;
| Tipo | Interpolación ${} | Multilínea | Uso habitual |
|---|---|---|---|
'simples' | No | No | Opción por defecto en este curso (y en muchas guías de estilo) |
"dobles" | No | No | Igual de válidas; habituales en JSON y en otros lenguajes |
`backticks` | Sí | Sí | Cuando necesitas insertar valores o varias líneas |
💡 Consejo: Elige un estilo y sé coherente; Prettier (módulo 21) puede aplicarlo automáticamente. En este curso usamos comillas simples y backticks cuando hay interpolación.
1.1. Secuencias de escape
| Secuencia | Significado |
|---|---|
\n | Salto de línea |
\t | Tabulador |
\\ | Barra invertida literal |
\' / \" / \` | Comilla literal |
\u00F1 | Carácter Unicode por su código (aquí, ñ) |
\u{1F600} | Code point de más de 4 dígitos hexadecimales (😀) |
console.log('Línea 1\nLínea 2');
// Línea 1
// Línea 2
console.log('Ruta: C:\\cursos\\js'); // Ruta: C:\cursos\js
console.log('Espa\u00F1a \u{1F600}'); // España 😀
2. Template literals
2.1. Interpolación y multilínea
const producto = 'Auriculares';
const precio = 49.9;
const unidades = 2;
// Antes: concatenación con + (propensa a errores con los espacios)
console.log('Total de ' + unidades + ' ' + producto + ': ' + precio * unidades + ' €');
// Con template literals: cualquier expresión dentro de ${}
console.log(`Total de ${unidades} ${producto}: ${(precio * unidades).toFixed(2)} €`);
// Total de 2 Auriculares: 99.80 €
console.log(`¿Hay stock? ${unidades > 0 ? 'Sí' : 'No'}`); // ¿Hay stock? Sí
// Multilínea: respeta los saltos de línea tal cual
const html = `
<article class="tarjeta">
<h2>${producto}</h2>
<p>${precio} €</p>
</article>`;
console.log(html.trim().split('\n').length); // 4
⚠️ Importante: Insertar texto del usuario directamente en HTML con template literals y
innerHTMLabre la puerta a ataques XSS. Lo trataremos en el módulo 14; el tagged template de la sección siguiente es una forma de protegerte.
2.2. Tagged templates
📘 Concepto: Un tagged template es una llamada a función con una sintaxis especial:
funcionseguida de un template literal, sin paréntesis. La función recibe un array con los trozos de texto fijos y, después, los valores interpolados, y puede devolver lo que quiera.
function etiqueta(textos, ...valores) {
console.log(textos); // trozos fijos (siempre uno más que valores)
console.log(valores); // valores interpolados
return 'resultado';
}
const nombre = 'Ana';
const edad = 30;
etiqueta`Hola ${nombre}, tienes ${edad} años`;
// ['Hola ', ', tienes ', ' años']
// ['Ana', 30]
Ejemplo útil: escapar automáticamente los valores al construir HTML.
const escaparHtml = (texto) =>
String(texto)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
function html(textos, ...valores) {
return textos.reduce((resultado, texto, i) => {
const valor = i < valores.length ? escaparHtml(valores[i]) : '';
return resultado + texto + valor;
}, '');
}
const comentario = '<img src=x onerror="alert(1)">';
console.log(html`<p class="comentario">${comentario}</p>`);
// <p class="comentario"><img src=x onerror="alert(1)"></p>
💡 Consejo: Bibliotecas como lit-html o los generadores de consultas SQL seguras usan exactamente esta técnica: el texto fijo lo escribe el programador (seguro) y los valores se tratan aparte.
El único tag que viene incluido es String.raw, que devuelve el texto sin procesar los escapes:
console.log(String.raw`C:\nuevo\tema`); // C:\nuevo\tema
console.log(`C:\nuevo\tema`); // C: (salto de línea) uevo (tabulador) ema
3. Inmutabilidad y acceso a caracteres
📘 Concepto: Las cadenas son inmutables: ningún método modifica la cadena original; todos devuelven una cadena nueva. Tampoco puedes cambiar un carácter por su índice.
let saludo = 'hola';
saludo.toUpperCase(); // devuelve 'HOLA', pero no lo guardamos
console.log(saludo); // hola
saludo = saludo.toUpperCase(); // hay que reasignar
console.log(saludo); // HOLA
try {
saludo[0] = 'h'; // intentar cambiar un carácter
} catch (error) {
console.log(error.message); // Cannot assign to read only property '0' of string 'HOLA'
}
⚠️ Importante: En los módulos ES (modo estricto), asignar a un índice de una cadena lanza
TypeError. En scripts clásicos sin modo estricto la asignación se ignora en silencio, lo que es aún peor porque no te enteras.
const lenguaje = 'JavaScript';
console.log(lenguaje[0]); // J
console.log(lenguaje.charAt(0)); // J
console.log(lenguaje.at(-1)); // t (índices negativos desde el final)
console.log(lenguaje[20]); // undefined
console.log(lenguaje.charAt(20)); // '' (cadena vacía)
console.log(lenguaje.length); // 10
4. Métodos de String
| Método | Qué hace | Ejemplo | Resultado |
|---|---|---|---|
length | Longitud (en code units, ver sección 5) | 'hola'.length | 4 |
toUpperCase() / toLowerCase() | Mayúsculas / minúsculas | 'Ñandú'.toUpperCase() | 'ÑANDÚ' |
trim() / trimStart() / trimEnd() | Quita espacios en los extremos | ' hola '.trim() | 'hola' |
padStart(n, r) / padEnd(n, r) | Rellena hasta longitud n | '7'.padStart(3, '0') | '007' |
includes(t) | ¿Contiene t? | 'JavaScript'.includes('Script') | true |
startsWith(t) / endsWith(t) | ¿Empieza / termina por t? | 'foto.png'.endsWith('.png') | true |
indexOf(t) / lastIndexOf(t) | Posición (o -1) | 'JavaScript'.indexOf('a') | 1 |
slice(ini, fin) | Trozo; admite negativos | 'abcdef'.slice(-3) | 'def' |
substring(ini, fin) | Trozo; negativos = 0 | 'abcdef'.substring(1, 3) | 'bc' |
split(sep) | Divide en array | 'a,b,c'.split(',') | ['a', 'b', 'c'] |
replace(a, b) | Reemplaza la primera | 'a-b-c'.replace('-', '+') | 'a+b-c' |
replaceAll(a, b) | Reemplaza todas | 'a-b-c'.replaceAll('-', '+') | 'a+b+c' |
repeat(n) | Repite n veces | 'ab'.repeat(3) | 'ababab' |
concat(...) | Concatena (mejor usa + o backticks) | 'Hola'.concat(' ', 'mundo') | 'Hola mundo' |
localeCompare(t, idioma) | Compara según idioma | 'a'.localeCompare('b') | -1 |
normalize(forma) | Normaliza Unicode (sección 5) | 'e\u0301'.normalize() | 'é' |
// Normalizar una entrada de usuario antes de compararla
const entrada = ' ANA@Correo.ES ';
const email = entrada.trim().toLowerCase();
console.log(email); // ana@correo.es
// Comprobar extensiones de fichero sin distinguir mayúsculas
const fichero = 'Informe_Final.PDF';
console.log(fichero.toLowerCase().endsWith('.pdf')); // true
// Extraer partes
const ruta = '/usuarios/42/pedidos';
console.log(ruta.split('/')); // ['', 'usuarios', '42', 'pedidos']
console.log(ruta.split('/').filter(Boolean).at(1)); // 42
// Formatear tablas en la consola
const filas = [['Pan', 1.2], ['Aceite', 8.75]];
for (const [articulo, precio] of filas) {
console.log(`${articulo.padEnd(10, '.')}${precio.toFixed(2).padStart(7)} €`);
}
// Pan....... 1.20 €
// Aceite.... 8.75 €
// Enmascarar una tarjeta
const tarjeta = '4111111111111111';
console.log(tarjeta.slice(-4).padStart(tarjeta.length, '*')); // ************1111
4.1. slice frente a substring
slice(ini, fin) | substring(ini, fin) | |
|---|---|---|
| Índices negativos | Cuentan desde el final | Se tratan como 0 |
ini > fin | Devuelve '' | Intercambia los índices |
| Recomendación | Úsalo por defecto (igual que en arrays) | Solo por compatibilidad con código antiguo |
console.log('abcdef'.slice(-3)); // def
console.log('abcdef'.substring(-3)); // abcdef
console.log('abc'.slice(2, 0)); // ''
console.log('abc'.substring(2, 0)); // ab
💡 Consejo: Olvídate de
substr(): está obsoleto (deprecated). Usaslice.
4.2. localeCompare y comparaciones sin acentos
console.log('a'.localeCompare('b')); // -1 (a va antes)
console.log('b'.localeCompare('a')); // 1
// sensitivity: 'base' ignora acentos y mayúsculas
console.log('canción'.localeCompare('CANCION', 'es', { sensitivity: 'base' })); // 0 (equivalentes)
// Búsqueda que ignora acentos: normalizar y quitar las marcas diacríticas
const quitarAcentos = (texto) => texto.normalize('NFD').replace(/\p{Diacritic}/gu, '');
console.log(quitarAcentos('Canción Ñoña')); // Cancion Nona
⚠️ Importante:
quitarAcentostambién convierte lañenn(porque lañse descompone enn+ virgulilla). Para búsquedas suele ser aceptable; para guardar datos, nunca.
5. Unicode, emojis y code points
📘 Concepto: JavaScript guarda las cadenas en UTF-16: cada posición es una code unit de 16 bits. Los caracteres más comunes ocupan una, pero muchos emojis y algunos símbolos necesitan dos (un par sustituto o surrogate pair). Y lo que una persona percibe como “un carácter” (un grafema) puede estar formado por varios code points.
| Nivel | Qué es | Cómo contarlo |
|---|---|---|
| Code unit | Bloque de 16 bits | texto.length |
| Code point | Un carácter Unicode | [...texto].length o Array.from(texto).length |
| Grafema | Lo que el usuario ve como un carácter | Intl.Segmenter |
console.log('hola'.length); // 4
console.log('😀'.length); // 2 ← ¡un emoji, dos code units!
console.log([...'😀'].length); // 1 (spread recorre por code points)
const familia = '👨👩👧'; // hombre + ZWJ + mujer + ZWJ + niña
console.log(familia.length); // 8
console.log([...familia].length); // 5
console.log(Array.from(familia)); // ['👨', '', '👩', '', '👧'] (los ZWJ son invisibles)
// Grafemas: lo que de verdad ve el usuario
const segmentador = new Intl.Segmenter('es', { granularity: 'grapheme' });
console.log([...segmentador.segment(familia)].length); // 1
console.log([...segmentador.segment('👍🏽')].length); // 1 (pulgar + tono de piel)
5.1. Recorrer cadenas correctamente
const texto = 'Hi😀';
// ❌ Por índice: rompe el emoji en dos mitades
for (let i = 0; i < texto.length; i++) {
console.log(texto[i]); // H, i, y dos caracteres "rotos" (�)
}
// ✅ for...of recorre por code points
for (const caracter of texto) {
console.log(caracter); // H, i, 😀
}
// Code points numéricos
console.log('😀'.codePointAt(0)); // 128512
console.log('😀'.codePointAt(0).toString(16)); // 1f600
console.log(String.fromCodePoint(0x1f600)); // 😀
💡 Consejo: Si tu aplicación limita la longitud de un texto (un nombre de usuario, un tuit), cuenta grafemas con
Intl.Segmenter. Si cortas conslicepor la mitad de un emoji, el resultado mostrará un símbolo�.
// Cortar un texto sin romper emojis ni grafemas
function truncar(texto, maximo) {
const grafemas = [...new Intl.Segmenter('es', { granularity: 'grapheme' }).segment(texto)];
if (grafemas.length <= maximo) return texto;
return grafemas.slice(0, maximo).map((s) => s.segment).join('') + '…';
}
console.log(truncar('Me encanta 👨👩👧 y 🍕', 12)); // Me encanta 👨👩👧…
Intl.Segmenter también sabe dividir en palabras y frases según el idioma:
const palabras = new Intl.Segmenter('es', { granularity: 'word' });
const frase = '¡Hola, mundo! ¿Qué tal?';
const soloPalabras = [...palabras.segment(frase)].filter((s) => s.isWordLike).map((s) => s.segment);
console.log(soloPalabras); // ['Hola', 'mundo', 'Qué', 'tal']
5.2. Normalización
La é puede escribirse como un solo code point (\u00E9) o como e + tilde combinable (e\u0301). Se ven iguales, pero no son iguales para ===:
const compuesta = '\u00E9'; // é
const descompuesta = 'e\u0301'; // é
console.log(compuesta === descompuesta); // false
console.log(compuesta.length, descompuesta.length); // 1 2
console.log(compuesta === descompuesta.normalize('NFC')); // true
💡 Consejo: Normaliza con
normalize('NFC')los textos que vayas a comparar o guardar (nombres de usuario, búsquedas). Así dos entradas visualmente idénticas serán también iguales en el código.
5.3. Cadenas mal formadas: isWellFormed y toWellFormed (ES2024)
Un par sustituto incompleto (un lone surrogate) da una cadena mal formada: puede aparecer al cortar mal un emoji. Algunas APIs fallan con ellas, como encodeURIComponent:
const roto = 'ab\uD83D'; // media pareja de un emoji
console.log(roto.isWellFormed()); // false
console.log('ab😀'.isWellFormed()); // true
try {
encodeURIComponent(roto);
} catch (error) {
console.log(error.name, error.message); // URIError URI malformed
}
// toWellFormed sustituye las mitades sueltas por U+FFFD (�)
console.log(encodeURIComponent(roto.toWellFormed())); // ab%EF%BF%BD
6. Expresiones regulares: fundamentos
📘 Concepto: Una expresión regular (regex) es un patrón que describe un conjunto de cadenas. Sirve para buscar, validar, extraer y reemplazar texto. Piensa en ella como en un “molde”:
\d{5}describe “cinco dígitos seguidos”, cualquiera que sean.
6.1. Crear una regex
// Literal: entre barras. La forma habitual cuando el patrón es fijo
const codigoPostal = /^\d{5}$/;
// Constructor: cuando el patrón se construye a partir de texto
const palabra = 'gato';
const busqueda = new RegExp(`\\b${palabra}\\b`, 'i'); // ¡las barras se duplican en la cadena!
console.log(codigoPostal.test('28013')); // true
console.log(busqueda.test('El GATO duerme')); // true
console.log(busqueda.source, busqueda.flags); // \bgato\b i
⚠️ Importante: Construir una regex con texto del usuario sin escaparlo es peligroso: si el usuario escribe
.o(, cambiará el significado del patrón o provocará un error. La solución moderna esRegExp.escape(sección 9).
6.2. Flags
| Flag | Nombre | Efecto |
|---|---|---|
g | global | Busca todas las coincidencias, no solo la primera |
i | ignoreCase | No distingue mayúsculas de minúsculas |
m | multiline | ^ y $ coinciden al principio y fin de cada línea |
s | dotAll | El punto . también coincide con saltos de línea |
u | unicode | Trata el patrón como code points; habilita \p{...} y \u{...} |
v | unicodeSets (ES2024) | Como u, más operaciones de conjuntos en clases (--, &&) |
d | hasIndices | Añade posiciones de inicio y fin de cada grupo (indices) |
y | sticky | Solo busca exactamente en la posición lastIndex |
💡 Consejo: Usa siempre
u(ov) cuando trabajes con texto que pueda contener acentos,ño emojis. Sin él, un emoji cuenta como dos “caracteres” para la regex.
6.3. Clases de caracteres
| Patrón | Coincide con |
|---|---|
. | Cualquier carácter salvo salto de línea (con s, también este) |
\d / \D | Un dígito 0-9 / cualquier cosa que no sea dígito |
\w / \W | Letra ASCII, dígito o _ / lo contrario. ⚠️ No incluye ñ ni acentos |
\s / \S | Espacio en blanco (espacio, tab, salto…) / lo contrario |
[abc] | Uno de: a, b o c |
[^abc] | Cualquiera excepto a, b, c |
[a-z0-9] | Un rango |
\p{L} (con u/v) | Cualquier letra de cualquier idioma |
\p{Lu} / \p{Ll} | Letra mayúscula / minúscula |
\b | Límite de palabra (posición, no carácter) |
console.log(/^[A-Z]/.test('Ángel')); // false (Á no está en A-Z)
console.log(/^\p{Lu}/u.test('Ángel')); // true
// Flag v: diferencia de conjuntos. Letras que NO sean minúsculas ASCII
console.log(/[\p{L}--[a-z]]/v.test('ñ')); // true
console.log(/[\p{L}--[a-z]]/v.test('a')); // false
6.4. Cuantificadores
| Cuantificador | Significado |
|---|---|
* | 0 o más |
+ | 1 o más |
? | 0 o 1 (opcional) |
{n} | Exactamente n |
{n,} | n o más |
{n,m} | Entre n y m |
*?, +?, ?? | Versiones perezosas (lazy): cogen lo mínimo posible |
const texto = '<b>uno</b> y <b>dos</b>';
console.log(texto.match(/<b>.*<\/b>/)[0]); // <b>uno</b> y <b>dos</b> (codicioso)
console.log(texto.match(/<b>.*?<\/b>/)[0]); // <b>uno</b> (perezoso)
6.5. Anclas y alternativas
| Patrón | Significado |
|---|---|
^ | Inicio de la cadena (o de línea con m) |
$ | Final de la cadena (o de línea con m) |
a\|b | a o b |
console.log(/\d{5}/.test('Mi CP es 28013, gracias')); // true (contiene 5 dígitos)
console.log(/^\d{5}$/.test('Mi CP es 28013')); // false (no es SOLO 5 dígitos)
console.log(/^(lunes|martes)$/i.test('Martes')); // true
⚠️ Importante: Para validar, ancla el patrón con
^...$. Sin anclas,/\d{5}/acepta'abc123456789'porque contiene cinco dígitos.
6.6. Grupos
// Grupo de captura (...) : guarda lo que coincide
const fecha = /(\d{2})\/(\d{2})\/(\d{4})/;
const m = '03/10/2026'.match(fecha);
console.log(m[0], m[1], m[2], m[3]); // 03/10/2026 03 10 2026
// Grupo con nombre (?<nombre>...): mucho más legible
const fechaNombrada = /(?<dia>\d{2})\/(?<mes>\d{2})\/(?<anio>\d{4})/;
const { dia, mes, anio } = '03/10/2026'.match(fechaNombrada).groups;
console.log(`${anio}-${mes}-${dia}`); // 2026-10-03
// Grupo sin captura (?:...): agrupa sin guardar
console.log(/^(?:\+34)?\s?[6789]\d{8}$/.test('+34 612345678')); // true
// Referencia hacia atrás: \1 (o \k<nombre>) repite lo capturado
console.log(/\b(\w+) \1\b/.test('esto es es un error')); // true (palabra repetida)
💡 Consejo: ES2025 permite además repetir el mismo nombre de grupo en alternativas distintas (
/(?<anio>\d{4})-\d{2}|\d{2}\/(?<anio>\d{4})/) y modificadores locales de flags, como(?i:...)para ignorar mayúsculas solo en una parte del patrón.
6.7. Lookahead y lookbehind
Comprueban lo que hay delante o detrás sin incluirlo en la coincidencia:
| Sintaxis | Nombre | Significa |
|---|---|---|
X(?=Y) | Lookahead positivo | X seguido de Y |
X(?!Y) | Lookahead negativo | X no seguido de Y |
(?<=Y)X | Lookbehind positivo | X precedido de Y |
(?<!Y)X | Lookbehind negativo | X no precedido de Y |
console.log('cuesta 45€'.match(/\d+(?=€)/)[0]); // 45 (números seguidos de €, sin el €)
console.log('cuesta €45'.match(/(?<=€)\d+/)[0]); // 45
console.log('-5 y 7'.match(/(?<!-)\b\d+/)[0]); // 7 (no precedido de signo menos)
// Separador de miles con lookahead
const miles = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, '.');
console.log(miles(1234567)); // 1.234.567
7. Métodos para usar regex
| Método | Devuelve | Uso |
|---|---|---|
regex.test(texto) | true / false | Validar |
regex.exec(texto) | Una coincidencia con detalles, o null | Extraer una a una |
texto.match(regex) | Sin g: como exec. Con g: array de coincidencias (o null) | Extraer |
texto.matchAll(regex) | Iterador de coincidencias con detalles (requiere g) | Extraer todas con grupos |
texto.search(regex) | Índice de la primera coincidencia o -1 | Localizar |
texto.replace(regex, r) | Nueva cadena | Reemplazar (todas con g) |
texto.replaceAll(regex, r) | Nueva cadena (requiere g) | Reemplazar todas |
texto.split(regex) | Array | Dividir por un patrón |
7.1. test, exec y match
const precios = 'Café 1.50€, tostada 2.25€ y zumo 3€';
console.log(/€/.test(precios)); // true
console.log(precios.match(/\d+(\.\d+)?/g)); // ['1.50', '2.25', '3']
console.log('sin números'.match(/\d+/g)); // null ← ¡no un array vacío!
// Sin g, match/exec dan detalles: índice, grupos...
const detalle = /(?<entero>\d+)\.(?<decimal>\d+)/.exec(precios);
console.log(detalle[0], detalle.index, detalle.groups.decimal); // 1.50 5 50
// Flag d: posiciones exactas de cada grupo
const conIndices = /(?<entero>\d+)\.(?<decimal>\d+)/d.exec(precios);
console.log(conIndices.indices.groups.decimal); // [7, 9]
⚠️ Importante:
matchcongdevuelvenullsi no hay coincidencias. Para usar el resultado sin comprobaciones, escribe(texto.match(/x/g) ?? []).length.
7.2. matchAll: todas las coincidencias con sus grupos
const log = `
2026-10-01 ERROR Disco lleno
2026-10-01 INFO Copia iniciada
2026-10-02 ERROR Tiempo de espera agotado`;
const patron = /^(?<fecha>\S+) (?<nivel>ERROR|INFO) (?<mensaje>.+)$/gm;
for (const { groups } of log.matchAll(patron)) {
if (groups.nivel === 'ERROR') console.log(`[${groups.fecha}] ${groups.mensaje}`);
}
// [2026-10-01] Disco lleno
// [2026-10-02] Tiempo de espera agotado
⚠️ Importante: Las regex con
g(oy) tienen estado: la propiedadlastIndex. Si reutilizas la misma regex contest, cada llamada continúa donde terminó la anterior:
const conG = /a/g;
console.log(conG.test('aa'), conG.test('aa'), conG.test('aa')); // true true false
// Solución: no uses g para validar con test, o crea la regex dentro de la función
7.3. replace con grupos y con función
// Patrones especiales en la cadena de reemplazo: $1, $<nombre>, $& (coincidencia completa)
console.log('03/10/2026'.replace(/(\d{2})\/(\d{2})\/(\d{4})/, '$3-$2-$1')); // 2026-10-03
console.log('03/10/2026'.replace(/(?<d>\d{2})\/(?<m>\d{2})\/(?<a>\d{4})/, '$<a>-$<m>-$<d>')); // 2026-10-03
console.log('Precio: 20'.replace(/\d+/, '[$&]')); // Precio: [20]
// Con función: calcula el reemplazo para cada coincidencia
const plantilla = 'Hola {nombre}, tu pedido {pedido} está {estado}.';
const datos = { nombre: 'Ana', pedido: 'A-17', estado: 'en camino' };
const mensaje = plantilla.replace(/\{(\w+)\}/g, (coincidencia, clave) => datos[clave] ?? coincidencia);
console.log(mensaje); // Hola Ana, tu pedido A-17 está en camino.
// De snake_case a camelCase
const aCamel = (texto) => texto.replace(/_(\p{L})/gu, (_, letra) => letra.toUpperCase());
console.log(aCamel('fecha_de_alta')); // fechaDeAlta
// Aplicar IVA a todos los precios de un texto
const ticket = 'Pan 1.20 €, Leche 0.95 €';
console.log(ticket.replace(/(\d+\.\d+) €/g, (_, n) => `${(n * 1.21).toFixed(2)} €`));
// Pan 1.45 €, Leche 1.15 €
💡 Consejo: La función de reemplazo recibe
(coincidencia, grupo1, grupo2, ..., posición, textoCompleto, grupos). Si usas grupos con nombre, el último argumento es el objetogroups.
7.4. split con regex
console.log('uno, dos;tres cuatro'.split(/[,;\s]+/)); // ['uno', 'dos', 'tres', 'cuatro']
// Si la regex tiene grupos de captura, los separadores se incluyen en el resultado
console.log('1+2-3'.split(/([+-])/)); // ['1', '+', '2', '-', '3']
8. Validaciones típicas
// Email sencillo: algo@algo.algo, sin espacios. (Validar emails al 100 % con regex es inviable:
// la validación definitiva es enviar un correo de confirmación.)
const esEmail = (texto) => /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(texto);
console.log(esEmail('ana@correo.es'), esEmail('ana@correo'), esEmail('a na@x.com')); // true false false
// Código postal español: 01000-52999 (provincias 01 a 52)
const esCodigoPostal = (cp) => /^(?:0[1-9]|[1-4]\d|5[0-2])\d{3}$/.test(cp);
console.log(esCodigoPostal('28013'), esCodigoPostal('08001'), esCodigoPostal('53000')); // true true false
// Teléfono móvil o fijo español, con prefijo opcional
const esTelefono = (t) => /^(?:\+34\s?)?[6-9]\d{2}\s?\d{3}\s?\d{3}$/.test(t);
console.log(esTelefono('+34 612 345 678'), esTelefono('912345678'), esTelefono('512345678')); // true true false
8.1. DNI con letra de control
La regex comprueba el formato (8 dígitos y una letra), y un cálculo comprueba la letra: es el resto de dividir el número entre 23, buscado en una tabla fija.
function esDniValido(dni) {
const resultado = /^(?<numero>\d{8})-?(?<letra>[A-Z])$/i.exec(dni.trim());
if (!resultado) return false;
const { numero, letra } = resultado.groups;
const letras = 'TRWAGMYFPDXBNJZSQVHLCKE';
return letras[Number(numero) % 23] === letra.toUpperCase();
}
console.log(esDniValido('12345678Z')); // true
console.log(esDniValido('12345678-z')); // true
console.log(esDniValido('12345678A')); // false (letra incorrecta)
console.log(esDniValido('1234567Z')); // false (7 dígitos)
8.2. Contraseña fuerte con lookaheads
Cada lookahead (?=...) comprueba una condición desde el inicio sin consumir caracteres:
const esFuerte = (clave) => /^(?=.*\p{Ll})(?=.*\p{Lu})(?=.*\d)(?=.*[^\p{L}\d\s]).{8,}$/u.test(clave);
console.log(esFuerte('Hola1234')); // false (falta símbolo)
console.log(esFuerte('Hola123!')); // true
console.log(esFuerte('Ñandú#2026')); // true
// Mejor para el usuario: decir QUÉ falta
const requisitos = [
[/\p{Ll}/u, 'una minúscula'],
[/\p{Lu}/u, 'una mayúscula'],
[/\d/, 'un número'],
[/[^\p{L}\d\s]/u, 'un símbolo'],
[/^.{8,}$/u, 'al menos 8 caracteres'],
];
const faltan = (clave) => requisitos.filter(([re]) => !re.test(clave)).map(([, texto]) => texto);
console.log(faltan('hola')); // ['una mayúscula', 'un número', 'un símbolo', 'al menos 8 caracteres']
💡 Consejo: Una lista de requisitos separados es más fácil de mantener y de explicar al usuario que una regex gigante. Y recuerda: validar en el navegador mejora la experiencia, pero siempre hay que validar también en el servidor.
9. RegExp.escape (ES2025): regex a partir de texto del usuario
Imagina un buscador que resalta lo que escribe el usuario. Si construyes la regex directamente, los caracteres especiales rompen el patrón:
const texto = 'El precio es 1.5+2 euros (aprox.)';
const busqueda = '1.5+2';
// ❌ Sin escapar: '.' significa "cualquier carácter" y '+' "uno o más"
console.log(new RegExp(busqueda).test('125552')); // true ← ¡coincidencia falsa!
// ❌ Y con un paréntesis suelto, directamente un error
try {
new RegExp('(aprox.');
} catch (error) {
console.log(error.name); // SyntaxError
}
// ✅ RegExp.escape convierte el texto en un patrón literal seguro
const patron = RegExp.escape(busqueda);
console.log(patron); // \x31\.5\+2
console.log(new RegExp(patron).test(texto)); // true
console.log(new RegExp(patron).test('125552')); // false
📘 Concepto:
RegExp.escapeescapa todos los caracteres con significado especial (. * + ? ^ $ { } ( ) | [ ] \ /) y algunos más por seguridad: el primer carácter si es una letra o un dígito (como\x31para1) y los espacios. Así el resultado es seguro en cualquier posición del patrón, incluso concatenado tras otro trozo.
// Resaltar todas las apariciones de lo que busca el usuario, sin distinguir mayúsculas
function resaltar(texto, termino) {
if (!termino) return texto;
const regex = new RegExp(RegExp.escape(termino), 'giu');
return texto.replace(regex, (encontrado) => `<mark>${encontrado}</mark>`);
}
console.log(resaltar('C++ y c++ no son C#', 'c++'));
// <mark>C++</mark> y <mark>c++</mark> no son C#
⚠️ Importante:
RegExp.escapeestá disponible en las versiones actuales de Chrome, Edge, Firefox y Safari, y en Node.js 24. Si necesitas dar soporte a navegadores antiguos, usa un polyfill (por ejemplo, el decore-js) o la función clásicatexto.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').
10. Herramientas y buenas prácticas
10.1. Probar regex
- regex101.com: elige el flavor ECMAScript (JavaScript), pega tu patrón y tus textos de prueba. Explica cada parte del patrón, muestra los grupos y tiene un depurador paso a paso.
- La consola del navegador o de Node: prueba rápidamente con
/patron/.test('texto'). - Tests automáticos (módulo 21): guarda casos que deben pasar y casos que deben fallar.
10.2. Buenas prácticas
| Práctica | Por qué |
|---|---|
Ancla con ^...$ al validar | Evita aceptar textos que solo contienen el patrón |
Usa el flag u o v | Texto internacional y emojis correctos, \p{L} disponible |
| Prefiere grupos con nombre | groups.anio se entiende; m[3] no |
Escapa la entrada del usuario con RegExp.escape | Evita errores y patrones manipulados |
| Divide regex enormes en varias pequeñas | Legibilidad y mensajes de error precisos |
| Usa métodos de String cuando basten | startsWith('http') es más claro que /^http/ |
Cuidado con cuantificadores anidados ((a+)+) | Pueden provocar backtracking catastrófico (ReDoS) y colgar la aplicación |
No uses g con test reutilizado | lastIndex provoca resultados alternos |
| No parsees HTML con regex | Usa el DOM (DOMParser, módulo 14) |
| Comenta las regex complejas | Dentro de unos meses no recordarás qué hacía |
// Una regex comentada: construirla desde partes con nombre
const PROVINCIA = '(?:0[1-9]|[1-4]\\d|5[0-2])';
const RESTO = '\\d{3}';
const CODIGO_POSTAL = new RegExp(`^${PROVINCIA}${RESTO}$`);
console.log(CODIGO_POSTAL.test('46001')); // true
11. Ejercicios
Ejercicio 1: Formateador de nombres
Escribe formatearNombre(texto) que reciba nombres mal escritos como ' mARÍA del cARMEN ' y devuelva 'María Del Carmen': quita espacios sobrantes (usa trim y split(/\s+/)), pon en mayúscula la primera letra de cada palabra y el resto en minúscula.
Ejercicio 2: Contador Unicode
Escribe analizar(texto) que devuelva un objeto con: codeUnits (length), codePoints (spread) y grafemas (Intl.Segmenter). Pruébala con 'Hola', '¡Olé!', '😀', '👨👩👧' y '👍🏽' y explica las diferencias en un comentario.
Ejercicio 3: Slug para URLs
Escribe slugify(titulo) que convierta '¡Aprende JavaScript en 2026: Guía Completa!' en 'aprende-javascript-en-2026-guia-completa': minúsculas, sin acentos (con normalize y \p{Diacritic}), cualquier secuencia de caracteres que no sean letras o números convertida en un guion, y sin guiones al principio ni al final.
Ejercicio 4: Tagged template de moneda
Crea un tag euros tal que euros`Total: ${12.5} y descuento ${3}` devuelva 'Total: 12,50 € y descuento 3,00 €'. Usa toLocaleString('es-ES', { minimumFractionDigits: 2 }) o toFixed y replace.
Ejercicio 5: Extraer datos de un texto
Dado un texto con varios emails, teléfonos y hashtags (invéntalo tú, con al menos 3 de cada), usa matchAll con grupos con nombre para extraer cada tipo en su propio array. Muestra los hashtags sin el # y sin repetir.
Ejercicio 6: Validador de formulario
Escribe validarRegistro({ nombre, email, cp, dni, clave }) que devuelva un objeto con los errores encontrados por campo (por ejemplo { email: 'Formato no válido', clave: ['una mayúscula', 'un símbolo'] }), reutilizando las funciones de la sección 8. El nombre debe contener solo letras (de cualquier idioma), espacios y guiones.
Ejercicio 7: Buscador con resaltado
Amplía resaltar de la sección 9 para que ignore acentos (buscar 'cancion' debe resaltar 'Canción'). Pista: normaliza texto y término para buscar las posiciones, pero resalta el texto original. Prueba con términos que contengan caracteres especiales como '(1.5)'.
Ejercicio 8: Mini plantillas
Escribe renderizar(plantilla, datos) que sustituya {{ clave }} (con espacios opcionales) y también rutas anidadas como {{ usuario.nombre }} buscando en datos. Si una clave no existe, deja un texto vacío. Usa replace con función y split('.') con reduce para recorrer la ruta.
Resumen
| Concepto | Sintaxis / ejemplo |
|---|---|
| Template literal | `Hola ${nombre}` |
| Tagged template | tag`texto ${valor}` → tag(textos, ...valores) |
| Texto sin escapes | String.raw`C:\nuevo` |
| Acceso | texto[0], texto.at(-1), texto.charAt(0) |
| Buscar | includes, startsWith, endsWith, indexOf |
| Extraer | slice(ini, fin), split(sep) |
| Transformar | trim, toUpperCase, padStart, replaceAll, repeat |
| Comparar | localeCompare(otro, 'es', { sensitivity: 'base' }), normalize('NFC') |
| Code points / grafemas | [...texto], codePointAt, new Intl.Segmenter('es', { granularity: 'grapheme' }) |
| Bien formada | isWellFormed(), toWellFormed() |
| Regex | /patrón/flags, new RegExp(texto, flags) |
| Flags | g i m s u v d y |
| Clases y cuantificadores | \d \w \s . [a-z] \p{L} · * + ? {n,m} · perezosos *? |
| Grupos | (...), (?<nombre>...), (?:...), \1 |
| Lookarounds | (?=...), (?!...), (?<=...), (?<!...) |
| Métodos | test, exec, match, matchAll, replace, split, search |
| Reemplazo | '$1', '$<nombre>', '$&', función (m, g1, ...) => ... |
| Escapar texto del usuario | new RegExp(RegExp.escape(texto), 'giu') |