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, charAt y []
  • Usar con soltura los métodos de String más importantes
  • Comprender Unicode: code units, code points, emojis y grafemas (Intl.Segmenter)
  • Detectar y reparar cadenas mal formadas con isWellFormed y toWellFormed
  • Escribir expresiones regulares: clases, cuantificadores, anclas, grupos y lookarounds
  • Usar los flags g, i, m, s, u, v, d e y
  • Aplicar test, exec, match, matchAll, replace y split con 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 innerHTML abre 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: funcion seguida 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('&', '&amp;')
    .replaceAll('<', '&lt;')
    .replaceAll('>', '&gt;')
    .replaceAll('"', '&quot;')
    .replaceAll("'", '&#39;');

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">&lt;img src=x onerror=&quot;alert(1)&quot;&gt;</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). Usa slice.

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: quitarAcentos también convierte la ñ en n (porque la ñ se descompone en n + 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 con slice por 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 es RegExp.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 (o v) 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: match con g devuelve null si 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 (o y) tienen estado: la propiedad lastIndex. Si reutilizas la misma regex con test, 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 objeto groups.

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.escape escapa 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 \x31 para 1) 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.escape está 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 de core-js) o la función clásica texto.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')