Guía de expresiones regulares y cadenas coincidentes en JavaScript

En esta guía, echaremos un vistazo a la clase RegExp y las expresiones regulares en JavaScript, así como a los métodos exec(), test(), match(), search() y replace() para hacer coincidir cadenas con patrones, con ejemplos

Introducción

Hacer coincidir cuerdas o patrones de cuerdas puede ser una verdadera lucha. En los casos más comunes, los necesitará para validar correos electrónicos, entradas de usuarios, nombres de archivos o la mayoría de los tipos de cadenas de entrada. Si bien hay muchas bibliotecas y enfoques de coincidencia de patrones, un enfoque probado en el tiempo es usar Expresiones regulares para definir un conjunto de reglas que debe seguir una determinada cadena para que coincida con ese patrón.

En JavaScript, la clase RegExp se usa para representar Expresiones regulares y se puede combinar con algunos métodos que facilitan la coincidencia de patrones.

Obviamente, el requisito previo para trabajar con estos es el conocimiento de expresiones regulares. Si no se siente cómodo al escribirlos, siempre puede usar sitios web de prueba de RegEx como Regex101.com o Regexr.com - que muestran visualmente los efectos de sus expresiones en cadenas dadas.

En esta guía, veremos las expresiones regulares en JavaScript, el uso de la clase RegExp, así como los métodos exec() y test().

Luego, veremos algunos de los métodos implementados con el objeto String: match(), search() y replace(), que funcionan con expresiones regulares como una alternativa más corta a utilizando la clase RegExp.

¿Qué son las expresiones regulares?

Antes de sumergirnos en la API de JavaScript para trabajar con RegEx, primero echemos un vistazo a las expresiones regulares. Si ya está familiarizado con ellos, esto puede servir como un repaso, o puede omitir la sección por completo.

Una Expresión regular (abreviatura RegEx) es un patrón de caracteres que se utiliza para hacer coincidir diferentes combinaciones de cadenas o caracteres. Hay ciertas reglas que debe seguir para formar una expresión regular adecuada. Los repasaremos rápidamente y seguiremos con un ejemplo:.

  • [abc] - coincide con un carácter único: a, b o c
  • [^abc] - coincide con todos los caracteres excepto a, b o c
  • [a-z] - coincide con cualquier carácter en el rango a-z
  • \s - coincide con cualquier carácter espacio en blanco
  • \w - coincide con cualquier carácter palabra

Estos son algunos de los patrones básicos pero te pueden llevar lejos. Las expresiones regulares también admiten operadores:

  • a? - El operador ? coincide con cero o uno carácter a
  • a* - el operador * coincide con cero o más caracteres a
  • a+ - el operador + coincide con uno o más caracteres a
  • a{n} - el operador {n} coincide con el carácter a exactamente n veces seguidas
  • a{n, m} - el operador {n, m} coincide con el carácter a entre n y m veces seguidas
  • \. - El operador \ escapa del carácter ., lo que significa que el carácter . no tendrá su significado habitual (coincidencia con cualquier cadena), pero se comparará como un carácter .

Para poner esto en práctica, escribamos una expresión regular que verifique si una cadena contiene @gmail.com al final de la cadena y si contiene tres caracteres a antes del símbolo @:

1
"\w+a{3}@gmail\.com"

Analicemos esto rápidamente:

  • \w - coincide con cualquier carácter
  • a{3} - coincide con tres caracteres a seguidos
  • @gmail\.com - coincide con una cadena literal "@gmail.com", mientras escapa el . con un operador \

Con este RegEx, podemos hacer coincidir cadenas como:

1
2
[correo electrónico protegido]
[correo electrónico protegido]

Pero no:

1
2
3
[correo electrónico protegido]
[correo electrónico protegido]
[correo electrónico protegido]

Puede seguir adelante y probarlos en un probador visual RegEx para ver qué partes coinciden y por qué.

La clase RegExp

En JavaScript, hay dos formas de crear una expresión regular:

  1. Usando un RegEx literal, que es un patrón colocado entre los caracteres /:
1
let regex = "/[abc]+/";

Debe usar este enfoque si su RegEx permanecerá constante a lo largo del script, porque este RegEx se compila cuando el script se carga automáticamente.

  1. Usando el constructor RegExp():
1
let regex = new RegExp("[abc]+");

Se prefiere este enfoque cuando RegEx es dinámico y puede cambiar a lo largo del ciclo de vida del script. Se compila en tiempo de ejecución, no en tiempo de carga.

Nota: A partir de ES6, también puede pasar un RegEx literal como argumento del constructor:

1
let regex = new RegExp(/[abc]+/);

Al trabajar con RegExp, también puede pasar banderas (caracteres con un significado) que alteran la forma en que se compara un patrón. Algunas de estas banderas son:

  • i - que denota insensible a mayúsculas y minúsculas, por lo que A y a son iguales cuando coinciden
1
2
// Matches both ABC and abc one or more times
let regex = new RegExp("[abc]+", "i"); 
  • g - que indica que todos los casos posibles coincidirán, no solo el primero que se encuentre

  • m - que denota el modo multilínea, que permite que el patrón coincida con una cadena escrita en varias líneas

1
2
3
4
let string = `
This string can also be matched with
Even though it's written in multiple lines
`

El constructor RegExp() se usa únicamente para crear un patrón para ser probado. Sin embargo, contiene dos métodos que pueden probar el patrón y compararlo si encaja: exec() y test().

El método exec()

El método exec(), sin mucha sorpresa, ejecuta una búsqueda en una cadena. Si hay una coincidencia, devuelve una matriz que contiene información sobre la coincidencia; de lo contrario, devuelve null.

Para manejar posibles valores nulos, puede usar el Operador coalescente nulo agregado a ECMAScript 2020.

Probémoslo en el ejemplo del correo electrónico: estamos comprobando si un correo electrónico termina en @gmail.com y contiene tres caracteres a consecutivos justo antes del dominio @gmail.

Además, usaremos la marca que no distingue entre mayúsculas y minúsculas:

1
2
3
4
5
6
7
let regex = new RegExp(/\w+a{3}@gmail\.com/, "i");

let result1 = regex.exec("[correo electrónico protegido]");
let result2 = regex.exec("[correo electrónico protegido]");

console.log(result1);
console.log(result2);

O puede aplicar el operador coalescente nulo para la seguridad null:

1
2
3
4
let regex = new RegExp(/\w+a{3}@gmail\.com/, "i");

let result1 = regex.exec("[correo electrónico protegido]") ?? 'No matched results';
let result2 = regex.exec("[correo electrónico protegido]") ?? 'No matched results';

Echemos un vistazo a la salida:

1
2
3
4
5
6
7
8
9
[ '[correo electrónico protegido]',
  index: 0,
  input: '[correo electrónico protegido]',
  groups: undefined ]
  
[ '[correo electrónico protegido]',
  index: 0,
  input: '[correo electrónico protegido]',
  groups: undefined ]

Esta matriz contiene varias cosas:

  1. La cadena coincidente
  2. El valor de índice a partir del cual comienzan las cadenas coincidentes
  3. La cadena de entrada
  4. La propiedad de grupos que contiene un objeto de todos los grupos de captura con nombre; en la mayoría de los casos, será indefinido

Si desea aislar solo la cadena coincidente sin la información adicional, puede imprimir

1
console.log(results[0])

Una característica interesante del método exec() es que recuerda el índice del carácter donde dejó de ejecutarse, así que básicamente, puedes llamar a este método una y otra vez, hasta que obtengas un null a cambio.

Esta propiedad se llama lastIndex. Para lograr esto, puede pasar una matriz de cadenas a exec() en lugar de una sola cadena.

Pasemos una matriz de tres cadenas; dos de los cuales serán emparejados y uno que no. Para obtener múltiples resultados, podemos recorrer la matriz y llamar a exec() hasta que obtengamos un null. Además, vamos a crear una matriz vacía matchedStrings y enviarle las cadenas coincidentes.

Nota: debes pasar el indicador g al constructor RegExp() para obtener todos los resultados, no solo el primero. De esta manera, evitará entrar en un bucle infinito, y a nadie le gustan los bucles infinitos.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
let regex = new RegExp(/\w+a{3}@gmail\.com/, "g");

let strings = ["[correo electrónico protegido]", "[correo electrónico protegido]", "[correo electrónico protegido]"];
let matchedStrings = [];

let result = regex.exec(strings);
if(result != null) {
    matchedStrings.push(result[0]);
}

while(result != null) {
    result = regex.exec(strings);
    if(result != null) {
        matchedStrings.push(result[0]);
    }
}

console.log(matchedStrings);

Esto resulta en:

1
 ["[correo electrónico protegido]", "[correo electrónico protegido]"]

Puede ver que nunca hicimos un seguimiento de un índice de la última cadena ejecutada en una matriz, pero exec() sabía dónde continuar su búsqueda. ¡Con buena pinta!

El método test()

El método test() es similar a exec() excepto que no devuelve una matriz que contiene información, sino un simple verdadero o falso. Realiza la misma búsqueda que exec() y si un patrón coincide con una cadena, devuelve true. De lo contrario, devuelve falso:

1
2
3
4
5
6
7
let regex = new RegExp(/\w+a{3}@gmail\.com/, "i");

let results = regex.test("[correo electrónico protegido]");
console.log(results); // Output: true

results = regex.test("[correo electrónico protegido]");
console.log(results); // Output: false

Este método no puede devolver un nulo, y puede usar los resultados para dictar más lógica condicional.

El método test() también recuerda el lastIndex de la ejecución, por lo que puede probar una matriz de cadenas. Sin embargo, si prueba la misma cadena dos veces, obtendrá resultados diferentes:

1
2
3
4
5
6
7
let regex = new RegExp(/\w+a{3}@gmail\.com/, "g"); // Remember the 'g' flag when working with multiple results

let results = regex.test("[correo electrónico protegido]");
console.log(results); // Output: true

results = regex.test("[correo electrónico protegido]");
console.log(results); // Output: false

La razón por la que obtenemos falso la segunda vez es porque lastIndex se ha movido al final de la cadena, por lo que cuando comienza a buscar por segunda vez, comienza al final de la cadena, y no hay nada con lo que coincidir. Por lo tanto, devuelve falso.

Tendrás que asegurarte de que no haya duplicados si estás usando test() para el comportamiento esperado.

El uso de test() con una matriz de cadenas es el mismo que exec(), excepto que imprimirá true/false. En la práctica, esto no se usa comúnmente, a menos que esté realizando un seguimiento de la cantidad de cadenas coincidentes.

El método match()

El método match() es el primero de los métodos String que veremos, y funciona bien con Expresiones regulares.
Toma un RegEx como argumento y devuelve una matriz de coincidencias o null si no hay ninguna, así que, en esencia, es muy similar a la API que el método exec() de una instancia RegEx:

1
2
3
4
5
6
let regex = new RegExp(/\w+a{3}@gmail\.com/, "g"); // Note the 'g' flag

let string = "[correo electrónico protegido]";
let resultArray = string.match(regex);

console.log(resultArray); // Output: [ '[correo electrónico protegido]' ]

Nota: Alternativamente, puede usar un RegEx literal aquí para acortar el código, ya que de todos modos está compilado en una instancia RegEx:

1
2
3
4
let string = "[correo electrónico protegido]";
let resultArray = string.match(/\w+a{3}@gmail\.com/);

console.log(resultArray); // Output: [ '[correo electrónico protegido]' ]

Para tener una mejor idea del método, cambiemos el RegEx a /[a-z]/ - para que coincida solo con caracteres en minúsculas:

1
2
3
4
5
6
let regex = new RegExp(/[a-z]/, "g"); // Note the 'g' flag

let string = "[correo electrónico protegido]";
let resultArray = string.match(regex);

console.log(resultArray);

Esto da como resultado una matriz de todos los caracteres en minúsculas en la cadena:

1
["s","o","m","e","m","a","i","l","a","a","a","g","m","a","i","l","c","o","m"]

El método buscar()

El método search() busca una coincidencia entre el patrón pasado y la cadena. Si se encuentra una coincidencia, se devuelve su índice. De lo contrario, el método devuelve -1:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let regex = new RegExp(/\w+a{3}@gmail\.com/, "g"); // Note the 'g' flag

let string = "some string that isn't matched [correo electrónico protegido]";
let result = string.search(regex);

console.log(result); // Output: 31

string = "It should return -1 with this string";
result = string.search(regex);

console.log(result); // Output: -1

Este método debe utilizarse cuando desee averiguar si se encuentra una coincidencia y su índice. Si solo quiere saber si se encuentra una coincidencia, debe usar test().

También puede extraer esta información del método exec(), pero eso requiere que coincida con un elemento en una matriz y esto devuelve un resultado analizable más fácilmente.

El método replace()

El método replace(to_replace, replace_with) devuelve una nueva cadena donde el patrón que coincide con to_replace se reemplaza con replace_with.

No cambia la cadena original ya que las cadenas son inmutables.

El argumento to_replace puede ser una cadena o una instancia de RegExp. Si es una cadena, solo se reemplazará la primera aparición, mientras que si es un RegExp, se reemplazarán todos y cada uno.

Para el propósito de este método, reemplacemos gmail.com con yahoo.com.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
let regex = new RegExp(/gmail\.com/, "g"); // Note the 'g' flag

let string = "[correo electrónico protegido]";
let result = string.replace(regex, "yahoo.com");

console.log(result); // Output: [correo electrónico protegido]

string = "[correo electrónico protegido] [correo electrónico protegido]"
result = string.replace(regex, "yahoo.com");

console.log(result); // Output: [correo electrónico protegido] [correo electrónico protegido]

console.log(string); // Output: [correo electrónico protegido] [correo electrónico protegido]

Como puede ver en el segundo ejemplo, todas las ocurrencias que coincidan con regex se reemplazan con yahoo.com. Además, la cadena original se deja sin cambios.

Conclusión

Aunque las expresiones regulares pueden ser difíciles de leer y, al principio, pueden ser difíciles de entender, después de entenderlas, trabajar con ellas y construirlas puede ser muy divertido.

JavaScript se aseguró de que las pruebas y las coincidencias fueran lo más fáciles posible, todo lo que necesita hacer es aprender las expresiones regulares.

Sin embargo, con las herramientas disponibles en la actualidad y con sitios similares a los que se enumeran al principio de la guía, puede aprender fácilmente todas las reglas de las expresiones regulares.

En esta guía, hemos cubierto:

  • La clase RegExp - una clase cuyo objeto se usa para representar una expresión regular
  • El método exec() - que busca una expresión regular en una cadena y devuelve una serie de coincidencias (con información adicional).
  • El método test() - que solo comprueba si hay una coincidencia en una cadena y devuelve verdadero/falso.
  • El método match() - definido en la clase String, devuelve una matriz de coincidencias (sin información adicional).
  • El método search() - definido en la clase String, devuelve un índice de una coincidencia encontrada.
  • El método replace() - definido en la clase String, reemplaza un RegExp() con una cadena.

Probablemente, la mejor práctica para las expresiones regulares es probar las de validación de correo electrónico y contraseña. *