RegExp lastIndex y flags: el estado que cambia respuestas
Pasa un patrón por test, exec y matchAll: cómo avanza lastIndex con las flags global y sticky, por qué un patrón global reutilizado cambia su respuesta, y cómo unicode cuenta.
BotBrowser Team
Quieres la documentación estructurada de Huella digital?
Este artículo forma parte de la biblioteca editorial. Para pasos de configuración, material de referencia y actualizaciones continuas, entra en la sección de docs.
BotBrowser soporta repetir un fixture autorizado del mismo origen bajo un perfil declarado y conservar observaciones acotadas sobre si un patrón coincidió y dónde, pero no puede cambiar cómo un entorno implementa el algoritmo RegExp, no puede hacer que un patrón global deje de llevar estado entre llamadas y no convierte una posición de coincidencia en una señal de identidad. Esta revisión pasa un conjunto de patrones por test, exec, match, replace y matchAll, y luego registra cómo avanza lastIndex con las flags global y sticky, por qué cada método de String reinicia o ignora ese estado de forma distinta, por qué un patrón global reutilizado cambia su respuesta sobre la misma entrada, y cómo la flag unicode cambia lo que cuentan un . y un cuantificador.
Resumen
- Un patrón global lleva un
lastIndexquetestyexecavanzan; tres llamadas atestsobre la misma cadena devuelventrue,truey luegofalse, porque la tercera empieza pasada la última coincidencia. - Un patrón no global mantiene
lastIndexen 0, así que tres llamadas atestsobre la misma cadena devuelven la misma respuesta. - La flag sticky ancla cada coincidencia en
lastIndex, así que/b/yfalla en el desplazamiento 0 sobreaby coincide solo cuandolastIndexes 1. String.matchyString.replacecon flag global reinicianlastIndexa 0 después, por lo que no filtran estado como untestreutilizado.matchAlllanzaTypeErrorcon un patrón no global, porque necesita la flag global para recorrer coincidencias./^.$/falla con un emoji y/^.$/ucoincide, porque sin la flag unicode.cuenta una unidad UTF-16 y el emoji son dos.- La fuente y las flags de un patrón son cadenas legibles estables, así que un registro puede guardarlas exactamente.
Contenido
- Dónde lastIndex lleva estado entre llamadas
- Cómo la flag sticky ancla en lastIndex
- Cómo los métodos de String reinician o ignoran el estado
- Dónde la flag unicode cambia lo que cuenta un patrón
- Por qué un patrón global reutilizado cambia su respuesta
- Registra una lectura de coincidencia
Dónde lastIndex lleva estado entre llamadas
Un patrón no es una función pura de su entrada cuando lleva una flag global o sticky. El objeto RegExp guarda un número lastIndex, y test y exec lo leen como la posición inicial y lo reescriben como la posición tras la coincidencia. La sonda llama a test tres veces sobre la misma cadena con un patrón global:
RegExp=function node=v26.9.0
fresh /a/g lastIndex=0
test("a a") -> true lastIndex=1
test("a a") again -> true lastIndex=3
test("a a") third -> false lastIndex=0
non-global lastIndex after test=true then lastIndex=0
La primera llamada busca desde el índice 0, encuentra la a en 0, devuelve true y pone lastIndex en 1. La segunda busca desde 1, encuentra la a en 2, devuelve true y pone lastIndex en 3. La tercera busca desde 3, no encuentra nada, devuelve false y reinicia lastIndex a 0. Tres llamadas, una cadena, dos respuestas distintas, y lo único que cambió es un número dentro del objeto patrón.
Ese es el defecto más común en una ruta de expresiones regulares: un patrón creado una vez en el ámbito del módulo con flag global y reutilizado en un bucle que lo lee como si fuera un predicado puro. Un validador que construye /^[a-z]+$/g una vez y llama a test sobre cada nombre enviado aceptará el primer nombre, rechazará el segundo que empieza donde terminó la última coincidencia del primero, y el nombre rechazado se ve idéntico al aceptado. El fallo es intermitente de una forma que sigue a la entrada anterior, lo que lo hace difícil de reproducir con un solo caso de prueba.
La línea no global es el control. Un patrón sin la flag global ignora lastIndex al entrar y lo deja en 0, así que tres llamadas a test sobre la misma cadena devuelven el mismo booleano cada vez. El estado vive en el objeto patrón, no en la cadena, así que dos patrones con la misma fuente pero flags distintas se comportan distinto sobre entrada idéntica. La regla que mantiene seguro un predicado es dejar fuera la flag global cuando el patrón solo se prueba, y añadirla solo cuando la intención es recorrer cada coincidencia con exec en un bucle.
Cómo la flag sticky ancla en lastIndex
La flag sticky es la hermana de la global con una regla de anclaje distinta. Donde un patrón global busca hacia adelante desde lastIndex, un patrón sticky coincide solo exactamente en lastIndex y falla si la coincidencia tendría que empezar más tarde. La sonda fija lastIndex a mano para mostrar ambos resultados sobre la misma cadena de dos caracteres:
sticky /b/y at 0 on "ab" -> false
sticky /b/y at 1 on "ab" -> true
exec loop indices on "a a a" -> 0,2,4
Con lastIndex en 0, el sticky /b/y contra ab falla, porque el carácter en el índice 0 es a, no b, y una coincidencia sticky no puede saltar a la b del índice 1. Con lastIndex en 1, el mismo patrón coincide, porque el carácter en el índice 1 es exactamente b. El patrón y la cadena nunca cambiaron; la regla de anclaje convirtió una búsqueda en una prueba de posición exacta, y la posición vino del estado que fijó quien llama.
Ese anclaje es lo que hace útil la flag sticky para tokenizar: un lexer avanza lastIndex a propósito tras cada token y espera que el siguiente patrón coincida exactamente donde terminó el token anterior, así que un hueco o un solapamiento es un error de sintaxis en vez de un salto silencioso. Es también lo que hace frágil un patrón sticky en código que asume una búsqueda: quien fija lastIndex en un desplazamiento obsoleto y espera una búsqueda hacia adelante obtiene un fallo que apunta al desplazamiento y no a la cadena.
La línea del bucle exec muestra la forma bien comportada de usar un patrón global. Como exec devuelve el objeto de coincidencia y avanza lastIndex, un bucle while que corre hasta que exec devuelva null recoge cada coincidencia en orden y termina con lastIndex de vuelta en 0. Los índices 0, 2, 4 son exactamente las posiciones de los tres caracteres a, y el bucle terminó porque el cuarto exec no encontró nada y reinició el estado. Un bucle que llama a exec una vez por iteración sin condición de terminación, o que comparte el patrón con código que también llama a test, rompe este contrato y salta o repite coincidencias.
Cómo los métodos de String reinician o ignoran el estado
Los métodos de String tratan lastIndex distinto de test y exec, y la diferencia es por qué el mismo patrón global es seguro en un lugar y no en otro. La sonda pone lastIndex en 2 y luego llama a match y replace:
before match: lastIndex=2
String.match with /g -> ["a","a","a"] lastIndex after=0
String.replace with /g -> "X X" lastIndex after=0
String.match con un patrón global ignora el lastIndex entrante, devuelve cada coincidencia de la cadena como un arreglo de solo las subcadenas coincidentes, y reinicia lastIndex a 0 después. String.replace con un patrón global reemplaza cada coincidencia, también reinicia lastIndex a 0, y devuelve la cadena reescrita. Ambos métodos dejan el patrón en un estado limpio, así que un patrón global que solo se usa por match y replace no filtra los cambios que produce un test reutilizado.
Esa limpieza es cómoda y también fácil de malinterpretar. Quien ejecuta replace sobre un patrón global y luego llama a test sobre el mismo patrón ve que test empieza desde 0, lo cual es correcto, pero quien fija lastIndex a propósito antes de un match esperando que la coincidencia empiece ahí se sorprende, porque match ignoró el desplazamiento. El desplazamiento que test y exec respetan no es el que respetan match y replace, así que una ruta de código que mezcla los dos puntos de entrada sobre un patrón global tiene dos nociones de "dónde empezar" en el mismo objeto.
matchAll es más estricto que match y exige la flag global en la puerta. La sonda lo llama sobre un patrón no global y obtiene una excepción, y luego recoge coincidencias de un patrón global con grupos de captura:
matchAll on non-global -> THREW TypeError
matchAll groups -> a=1@0 b=2@3 c=3@6
matchAll lanza TypeError con un patrón no global porque devuelve un iterador que recorre cada coincidencia y necesita la flag global para definir ese recorrido. Sobre un patrón global rinde un objeto de coincidencia por coincidencia, con los grupos de captura y el índice de cada coincidencia, así que el a=1@0 b=2@3 c=3@6 recogido muestra tres coincidencias con sus dos grupos y sus posiciones iniciales. A diferencia de un bucle while sobre exec, matchAll no deja a quien llama la responsabilidad de avanzar lastIndex, así que es la forma más segura de recorrer cada coincidencia de un patrón que puede usarse también en otro lugar.
Dónde la flag unicode cambia lo que cuenta un patrón
La flag unicode cambia la unidad que un patrón cuenta de una unidad de código UTF-16 a un punto de código Unicode, y eso cambia tanto . como cualquier cuantificador sobre un carácter. La sonda prueba un patrón de un carácter y un cuantificador de dos caracteres contra un emoji:
/^.$/.test("😀") -> false
/^.$/u.test("😀") -> true
/^.{2}$/u.test("😀") -> false (length=2)
Sin la flag unicode, . coincide con una unidad de código UTF-16, y el emoji es un par sustituto de dos unidades, así que /^.$/ no puede coincidir con una cadena de dos unidades con un patrón de una unidad y devuelve false. Con la flag unicode, . coincide con un punto de código, y el emoji es un solo punto de código, así que /^.$/u devuelve true. El mismo emoji que el patrón simple ve como dos unidades el patrón unicode lo ve como un carácter, y un validador construido sobre /^.$/ rechaza un emoji que un lector cuenta como un carácter.
La tercera línea muestra la trampa inversa. Un cuantificador escrito como {2} bajo la flag unicode cuenta dos puntos de código, no dos unidades, así que /^.{2}$/u contra el emoji de un solo punto de código devuelve false aunque el .length de la cadena sea 2. Quien lee String.length (que siempre es la cuenta de unidades UTF-16) y escribe un cuantificador para igualarlo obtiene un patrón que discrepa de la propia longitud de la cadena bajo la flag unicode. Es el mismo hueco de unidad frente a carácter que registra un decodificador de texto, y es un lugar donde un patrón cuenta en silencio algo distinto de lo que asumió el código que lo escribió.
Juntas, las dos líneas dicen que la unidad que un patrón cuenta es una propiedad de la flag, no del texto del patrón. Una comprobación de longitud, un cuantificador .{n} y un . cuentan todos unidades UTF-16 sin la flag y puntos de código con ella, así que un patrón que mezcla una comprobación de String.length contra un patrón u compara dos unidades distintas. Registrar la flag junto al patrón es lo que permite a un lector posterior reproducir la misma cuenta.
Por qué un patrón global reutilizado cambia su respuesta
La forma más peligrosa del defecto de estado es un patrón global reutilizado entre llamadas, porque produce respuestas alternas sobre entrada idéntica. La sonda llama a test tres veces sobre la cadena 1 con un /\d/g global:
reused /\d/g test three times on "1" -> true,false,true
La primera llamada coincide con el dígito, pone lastIndex en 1 y devuelve true. La segunda empieza en 1, la cadena mide 1, así que no queda nada que buscar, devuelve false y reinicia lastIndex a 0. La tercera empieza desde 0 otra vez y devuelve true. El patrón es correcto, la cadena nunca cambió, y la respuesta alterna true, false, true solo porque el patrón recuerda dónde se detuvo.
Un patrón de coincidencia vacía necesita una regla extra, y la sonda muestra el resultado de aplicarla:
empty-match walk -> [0:"a"] [1:""] [2:""] [3:""]
/a*/g puede coincidir con una cadena vacía en cada posición, y exec haría un bucle infinito si lastIndex no avanzara más allá de una coincidencia de longitud cero. La sonda comprueba si lastIndex sigue igual al índice de coincidencia y, si es así, lo incrementa, y por eso el recorrido avanza [0:"a"], luego una coincidencia vacía en 1, en 2 y en 3 en vez de repetir en el mismo desplazamiento. Sin ese incremento manual, un bucle exec sobre un patrón de coincidencia vacía nunca termina, porque cada llamada devuelve una coincidencia de longitud cero en el mismo lastIndex y la posición nunca se mueve.
La solución al defecto del patrón reutilizado es mantener el estado fuera del objeto compartido. Un patrón global usado solo dentro de un único bucle while sobre exec está bien, porque ese bucle es dueño del lastIndex. Un patrón global usado como predicado reutilizable está mal, y la solución es quitar la flag global o reiniciar lastIndex a 0 antes de cada test. Las propiedades source y flags son la forma barata de registrar qué era un patrón, porque son cadenas legibles estables que sobreviven a un viaje por un registro.
Registra una lectura de coincidencia
Una lectura de coincidencia sostiene una afirmación estrecha: este entorno, este texto de patrón, estas flags, dada esta entrada, produjeron esta coincidencia en esta posición. Seis campos la mantienen verificable, y ninguno depende del estado privado de un objeto compartido:
- La
sourcedel patrón y susflags, registradas como las dos cadenas que expone el objeto, porque el conjunto de flags decide tanto la unidad que un patrón cuenta como si lleva estado. - La cadena de entrada tal como se suministró, idealmente con sus puntos de código en vez de sus glifos renderizados, porque dos entradas pueden renderizar igual y un patrón cuenta unidades o puntos de código según sus flags.
- El punto de entrada usado para correr el patrón,
test,exec,match,replaceomatchAll, porque cada uno tratalastIndexde forma distinta. - El
lastIndexantes y después de la llamada cuando el patrón es global o sticky, porque es el número donde vive el estado y una respuesta guardada sin él no se puede reproducir. - El índice de coincidencia y los grupos capturados, en vez de solo la subcadena coincidente, porque dos coincidencias del mismo texto en posiciones distintas son observaciones distintas.
- Si una coincidencia de longitud cero requirió un incremento manual de
lastIndex, porque un patrón de coincidencia vacía necesita ese paso para terminar y un registro que lo omite describe un bucle que se habría colgado.
Lo que tal lectura no puede mostrar es nada sobre cómo un servidor u otro entorno interpretará el mismo patrón. Un patrón lo corre un motor con un conjunto de semánticas de flags, y un proxy, un WAF o un framework de servidor puede analizar la misma cadena con sus propias reglas. Para las capas de bytes sobre las que corre a menudo un patrón, TextDecoder y TextEncoder muestra por qué los mismos bytes decodifican a una cadena bajo una etiqueta y a una cadena mal codificada bajo otra, y ArrayBuffer y TypedArray muestra por qué los mismos ocho bytes llevan un conteo de elementos distinto bajo cada vista. Un patrón sobre cualquiera de los dos tipos de datos hereda el hueco de unidad que miden esos artículos.
Dos hábitos mantienen la lectura honesta con el tiempo. Primero, registra las flags y el punto de entrada junto a la respuesta, porque un true de un test global y un true de un test no global significan cosas distintas sobre la llamada siguiente. Segundo, prefiere matchAll o un único bucle while sobre exec para recorrer coincidencias y un patrón no global para un predicado reutilizable, para que el estado que cambia respuestas nunca llegue a un segundo llamador que asumió una función pura.
Fuentes públicas
- MDN: RegExp
- MDN: RegExp.prototype.test
- MDN: RegExp.prototype.exec
- MDN: String.prototype.matchAll
- MDN: flag sticky de RegExp
- BotBrowser: referencia de consistencia de automatización
Conclusión
Trata el conjunto de flags como parte del contrato de datos: un patrón global o sticky lleva un lastIndex que test y exec avanzan, así que tres llamadas sobre una cadena devuelven true, true y luego false, mientras un patrón no global mantiene lastIndex en 0 y devuelve la misma respuesta cada vez. Sabe que la flag sticky ancla en lastIndex, que match y replace reinician lastIndex a 0 y matchAll exige la flag global, y que la flag unicode hace que . y un cuantificador cuenten puntos de código donde el defecto cuenta unidades UTF-16. Deja fuera la flag global de un predicado reutilizable, recorre coincidencias con matchAll o un bucle exec que sea dueño del estado, y registra la fuente, las flags, el punto de entrada y el lastIndex antes y después para que una respuesta guardada se pueda reproducir en vez de malinterpretarse como resultado puro.
Lectura siguiente
Para la capa de texto sobre la que suele correr un patrón, lee TextDecoder y TextEncoder; para las vistas binarias que un patrón puede escanear, lee ArrayBuffer y TypedArray; para el orden que un serializador da a las claves del objeto que un patrón casó, lee Orden de claves.
Este artículo forma parte de la guía: ¿Qué es la huella digital del navegador?
Artículos Relacionados
Unicode: cuatro identidades de texto que lee una página
Huella digitalstructuredClone frente a JSON: qué conserva una copia profunda
Huella digitalFuente aleatoria de crypto-js: qué rama usa tu build
Huella digitalua-parser-js: Client Hints, bots y un tope de 500
Huella digital¿Qué es la huella digital del navegador?
IdentidadHistorial de navegación sembrado: sesión y global
Lleva BotBrowser de la investigación a producción
Usa estas guías para entender el modelo y después avanzar hacia validación multiplataforma, contextos aislados y despliegue de navegador preparado para escalar.