RegExp lastIndex et flags : l’état qui compte
Passez un motif par test, exec et matchAll : comment lastIndex avance avec les flags global et sticky, pourquoi un motif global réutilisé change sa réponse, et unicode.
BotBrowser Team
Vous voulez la documentation structurée pour Empreinte ?
Cet article fait partie de la bibliothèque éditoriale. Pour les étapes de configuration, la référence et les mises à jour continues, passez directement à la section docs.
BotBrowser prend en charge la relecture d’un fixture autorisé du même origine sous un profil déclaré et conserve des observations bornées sur le fait qu’un motif a coïncidé et où, mais il ne peut pas changer la façon dont un environnement implémente l’algorithme RegExp, ne peut pas faire qu’un motif global cesse de porter un état entre les appels et ne fait pas d’une position de correspondance un signal d’identité. Cette revue passe un ensemble de motifs par test, exec, match, replace et matchAll, puis consigne comment lastIndex avance avec les flags global et sticky, pourquoi chaque méthode de String réinitialise ou ignore cet état différemment, pourquoi un motif global réutilisé change sa réponse sur la même entrée, et comment le flag unicode change ce que comptent un . et un quantificateur.
Résumé
- Un motif global porte un
lastIndexquetestetexecfont avancer ; trois appels àtestsur la même chaîne renvoienttrue,true, puisfalse, parce que le troisième commence après la dernière correspondance. - Un motif non global garde
lastIndexà 0, donc trois appels àtestsur la même chaîne renvoient la même réponse. - Le flag sticky ancre chaque correspondance à
lastIndex, donc/b/yéchoue à l’offset 0 surabet ne coïncide qu’une foislastIndexà 1. String.matchetString.replaceavec un flag global réinitialisentlastIndexà 0 ensuite, ce qui explique qu’ils ne laissent pas fuir l’état comme untestréutilisé.matchAlllèveTypeErrorsur un motif non global, parce qu’il a besoin du flag global pour parcourir les correspondances./^.$/échoue sur un emoji et/^.$/ucoïncide, parce que sans le flag unicode.compte une unité UTF-16 et l’emoji en fait deux.- La source et les flags d’un motif sont des chaînes lisibles stables, donc un registre peut les stocker exactement.
Sommaire
- Où lastIndex porte un état entre les appels
- Comment le flag sticky ancre à lastIndex
- Comment les méthodes de String réinitialisent ou ignorent l’état
- Où le flag unicode change ce que compte un motif
- Pourquoi un motif global réutilisé change sa réponse
- Consigner une lecture de correspondance
Où lastIndex porte un état entre les appels
Un motif n’est pas une fonction pure de son entrée quand il porte un flag global ou sticky. L’objet RegExp garde un nombre lastIndex, et test et exec le lisent comme position de départ et le réécrivent comme position après la correspondance. La sonde appelle test trois fois sur la même chaîne avec un motif 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
Le premier appel cherche depuis l’index 0, trouve le a à 0, renvoie true et met lastIndex à 1. Le deuxième cherche depuis 1, trouve le a à 2, renvoie true et met lastIndex à 3. Le troisième cherche depuis 3, ne trouve rien, renvoie false et réinitialise lastIndex à 0. Trois appels, une chaîne, deux réponses différentes, et la seule chose qui a changé est un nombre à l’intérieur de l’objet motif.
C’est le défaut le plus courant d’un chemin de code à expressions régulières : un motif créé une fois au niveau du module avec un flag global et réutilisé dans une boucle qui le lit comme un prédicat pur. Un validateur qui construit /^[a-z]+$/g une fois et appelle test sur chaque nom soumis acceptera le premier nom, puis rejettera le second qui commence là où s’est terminée la dernière correspondance du premier, et le nom rejeté est identique à l’accepté. L’échec est intermittent d’une façon qui suit l’entrée précédente, ce qui le rend difficile à reproduire avec un seul cas de test.
La ligne non globale est le contrôle. Un motif sans le flag global ignore lastIndex à l’entrée et le laisse à 0, donc trois appels à test sur la même chaîne renvoient le même booléen chaque fois. L’état vit dans l’objet motif, pas dans la chaîne, donc deux motifs de même source mais de flags différents se comportent différemment sur une entrée identique. La règle qui garde un prédicat sûr est de laisser de côté le flag global quand le motif ne sert qu’à tester, et de l’ajouter seulement quand l’intention est de parcourir chaque correspondance avec exec dans une boucle.
Comment le flag sticky ancre à lastIndex
Le flag sticky est le frère du global avec une règle d’ancrage différente. Là où un motif global cherche en avant depuis lastIndex, un motif sticky ne coïncide qu’exactement à lastIndex et échoue si la correspondance devrait commencer plus tard. La sonde fixe lastIndex à la main pour montrer les deux issues sur la même chaîne de deux caractères :
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
Avec lastIndex à 0, le sticky /b/y contre ab échoue, parce que le caractère à l’index 0 est a, pas b, et une correspondance sticky ne peut pas sauter au b de l’index 1. Avec lastIndex à 1, le même motif coïncide, parce que le caractère à l’index 1 est exactement b. Le motif et la chaîne n’ont jamais changé ; la règle d’ancrage a fait d’une recherche un test de position exacte, et la position vient de l’état fixé par l’appelant.
Cet ancrage est ce qui rend le flag sticky utile pour tokeniser : un lexer avance lastIndex volontairement après chaque token et attend que le motif suivant coïncide exactement où le token précédent s’est terminé, donc un trou ou un chevauchement est une erreur de syntaxe plutôt qu’un saut silencieux. C’est aussi ce qui rend un motif sticky fragile dans du code qui suppose une recherche : qui fixe lastIndex à un offset périmé et attend une recherche en avant obtient un échec qui pointe l’offset plutôt que la chaîne.
La ligne de la boucle exec montre la façon bien élevée d’utiliser un motif global. Comme exec renvoie l’objet de correspondance et avance lastIndex, une boucle while qui tourne jusqu’à ce que exec renvoie null recueille chaque correspondance dans l’ordre et se termine avec lastIndex de retour à 0. Les indices 0, 2, 4 sont exactement les positions des trois caractères a, et la boucle s’est arrêtée parce que le quatrième exec n’a rien trouvé et a réinitialisé l’état. Une boucle qui appelle exec une fois par itération sans condition d’arrêt, ou qui partage le motif avec du code qui appelle aussi test, rompt ce contrat et saute ou répète des correspondances.
Comment les méthodes de String réinitialisent ou ignorent l’état
Les méthodes de String traitent lastIndex différemment de test et exec, et la différence explique pourquoi le même motif global est sûr à un endroit et pas à un autre. La sonde met lastIndex à 2 puis appelle match et 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 avec un motif global ignore le lastIndex entrant, renvoie chaque correspondance de la chaîne comme un tableau des seules sous-chaînes correspondantes, et réinitialise lastIndex à 0 ensuite. String.replace avec un motif global remplace chaque correspondance, réinitialise aussi lastIndex à 0, et renvoie la chaîne réécrite. Les deux méthodes laissent le motif dans un état propre, donc un motif global qui n’est jamais utilisé qu’au travers de match et replace ne laisse pas fuir les bascules que produit un test réutilisé.
Ce nettoyage est commode et il est aussi facile à mal lire. Qui exécute replace sur un motif global puis appelle test sur le même motif voit test commencer à 0, ce qui est correct, mais qui fixe lastIndex exprès avant un match en attendant que la correspondance commence là est surpris, parce que match a ignoré l’offset. L’offset que test et exec respectent n’est pas celui que respectent match et replace, donc un chemin de code qui mélange les deux points d’entrée sur un motif global a deux notions de « où commencer » dans le même objet.
matchAll est plus strict que match et exige le flag global à l’entrée. La sonde l’appelle sur un motif non global et obtient une exception, puis recueille des correspondances d’un motif global avec des groupes de capture :
matchAll on non-global -> THREW TypeError
matchAll groups -> a=1@0 b=2@3 c=3@6
matchAll lève TypeError sur un motif non global parce qu’il renvoie un itérateur qui parcourt chaque correspondance et qu’il a besoin du flag global pour définir ce parcours. Sur un motif global il rend un objet de correspondance par correspondance, avec les groupes de capture et l’index de chaque correspondance, donc le a=1@0 b=2@3 c=3@6 recueilli montre trois correspondances avec leurs deux groupes et leurs positions de départ. Contrairement à une boucle while sur exec, matchAll ne laisse pas à l’appelant la responsabilité d’avancer lastIndex, c’est donc la façon la plus sûre de parcourir chaque correspondance d’un motif qui peut aussi servir ailleurs.
Où le flag unicode change ce que compte un motif
Le flag unicode change l’unité qu’un motif compte, d’une unité de code UTF-16 à un point de code Unicode, et cela change à la fois . et tout quantificateur sur un caractère. La sonde teste un motif d’un caractère et un quantificateur de deux caractères contre un emoji :
/^.$/.test("😀") -> false
/^.$/u.test("😀") -> true
/^.{2}$/u.test("😀") -> false (length=2)
Sans le flag unicode, . coïncide avec une unité de code UTF-16, et l’emoji est une paire de substitution de deux unités, donc /^.$/ ne peut pas coïncider avec une chaîne de deux unités avec un motif d’une unité et renvoie false. Avec le flag unicode, . coïncide avec un point de code, et l’emoji est un seul point de code, donc /^.$/u renvoie true. Le même emoji que le motif simple voit comme deux unités, le motif unicode le voit comme un caractère, et un validateur bâti sur /^.$/ rejette un emoji qu’un lecteur compte comme un caractère.
La troisième ligne montre le piège inverse. Un quantificateur écrit comme {2} sous le flag unicode compte deux points de code, pas deux unités, donc /^.{2}$/u contre l’emoji d’un seul point de code renvoie false bien que le .length de la chaîne soit 2. Qui lit String.length (qui est toujours le compte d’unités UTF-16) et écrit un quantificateur pour l’égaler obtient un motif qui diverge de la longueur même de la chaîne sous le flag unicode. C’est le même écart unité contre caractère qu’enregistre un décodeur de texte, et c’est un endroit où un motif compte en silence autre chose que ce que le code qui l’a écrit a supposé.
Ensemble, les deux lignes disent que l’unité qu’un motif compte est une propriété du flag, pas du texte du motif. Un contrôle de longueur, un quantificateur .{n} et un . comptent tous des unités UTF-16 sans le flag et des points de code avec lui, donc un motif qui mélange un contrôle de String.length contre un motif u compare deux unités différentes. Consigner le flag à côté du motif est ce qui permet à un lecteur ultérieur de reproduire le même compte.
Pourquoi un motif global réutilisé change sa réponse
La forme la plus dangereuse du défaut d’état est un motif global réutilisé entre les appels, parce qu’il produit des réponses alternées sur une entrée identique. La sonde appelle test trois fois sur la chaîne 1 avec un /\d/g global :
reused /\d/g test three times on "1" -> true,false,true
Le premier appel coïncide avec le chiffre, met lastIndex à 1 et renvoie true. Le deuxième commence à 1, la chaîne fait 1, donc il ne reste rien à chercher, il renvoie false et réinitialise lastIndex à 0. Le troisième commence depuis 0 de nouveau et renvoie true. Le motif est correct, la chaîne n’a jamais changé, et la réponse alterne true, false, true uniquement parce que le motif se souvient où il s’est arrêté.
Un motif de correspondance vide a besoin d’une règle de plus, et la sonde montre le résultat de son application :
empty-match walk -> [0:"a"] [1:""] [2:""] [3:""]
/a*/g peut coïncider avec une chaîne vide à chaque position, et exec boucherait sans fin si lastIndex n’avançait pas au-delà d’une correspondance de longueur nulle. La sonde vérifie si lastIndex est encore égal à l’index de correspondance et, si oui, l’incrémente, ce qui explique que le parcours avance [0:"a"], puis une correspondance vide à 1, à 2 et à 3 au lieu de répéter au même offset. Sans cet incrément manuel, une boucle exec sur un motif de correspondance vide ne termine jamais, parce que chaque appel renvoie une correspondance de longueur nulle au même lastIndex et la position ne bouge jamais.
Le correctif du défaut de motif réutilisé est de garder l’état hors de l’objet partagé. Un motif global utilisé uniquement dans une seule boucle while sur exec convient, parce que cette boucle possède le lastIndex. Un motif global utilisé comme prédicat réutilisable est faux, et le correctif est soit de retirer le flag global, soit de réinitialiser lastIndex à 0 avant chaque test. Les propriétés source et flags sont le moyen peu coûteux de consigner ce qu’était un motif, parce que ce sont des chaînes lisibles stables qui survivent à un aller-retour par un registre.
Consigner une lecture de correspondance
Une lecture de correspondance soutient une affirmation étroite : cet environnement, ce texte de motif, ces flags, pour cette entrée, ont produit cette correspondance à cette position. Six champs la gardent vérifiable, et aucun ne dépend de l’état privé d’un objet partagé :
- La
sourcedu motif et sesflags, consignées comme les deux chaînes que l’objet expose, parce que le jeu de flags décide à la fois de l’unité qu’un motif compte et de s’il porte un état. - La chaîne d’entrée telle que fournie, idéalement avec ses points de code plutôt que ses glyphes rendus, parce que deux entrées peuvent rendre pareil et un motif compte des unités ou des points de code selon ses flags.
- Le point d’entrée utilisé pour lancer le motif,
test,exec,match,replaceoumatchAll, parce que chacun traitelastIndexdifféremment. - Le
lastIndexavant et après l’appel quand le motif est global ou sticky, parce que c’est le nombre où vit l’état et une réponse stockée sans lui ne se reproduit pas. - L’index de correspondance et les groupes capturés, plutôt que seulement la sous-chaîne correspondante, parce que deux correspondances du même texte à des positions différentes sont des observations différentes.
- Si une correspondance de longueur nulle a exigé un incrément manuel de
lastIndex, parce qu’un motif de correspondance vide a besoin de cette étape pour terminer et un registre qui l’omet décrit une boucle qui aurait bloqué.
Ce qu’une telle lecture ne peut pas montrer, c’est quoi que ce soit sur la façon dont un serveur ou un second environnement interprétera le même motif. Un motif est lancé par un moteur avec un jeu de sémantiques de flags, et un proxy, un WAF ou un framework serveur peut analyser la même chaîne avec ses propres règles. Pour les couches d’octets sur lesquelles un motif tourne souvent, TextDecoder et TextEncoder montre pourquoi les mêmes octets décodent en une chaîne sous une étiquette et en une chaîne mal encodée sous une autre, et ArrayBuffer et TypedArray montre pourquoi les mêmes huit octets portent un compte d’éléments différent sous chaque vue. Un motif sur l’un ou l’autre type de données hérite de l’écart d’unité que mesurent ces articles.
Deux habitudes gardent la lecture honnête avec le temps. D’abord, consignez les flags et le point d’entrée à côté de la réponse, parce qu’un true d’un test global et un true d’un test non global ne veulent pas dire la même chose pour l’appel suivant. Ensuite, préférez matchAll ou une seule boucle while sur exec pour parcourir les correspondances et un motif non global pour un prédicat réutilisable, pour que l’état qui change les réponses n’atteigne jamais un second appelant qui a supposé une fonction pure.
Sources publiques
- MDN : RegExp
- MDN : RegExp.prototype.test
- MDN : RegExp.prototype.exec
- MDN : String.prototype.matchAll
- MDN : flag sticky de RegExp
- BotBrowser : référence de cohérence d’automatisation
Conclusion
Traitez le jeu de flags comme faisant partie du contrat de données : un motif global ou sticky porte un lastIndex que test et exec font avancer, donc trois appels sur une chaîne renvoient true, true, puis false, tandis qu’un motif non global garde lastIndex à 0 et renvoie la même réponse chaque fois. Sachez que le flag sticky ancre à lastIndex, que match et replace réinitialisent lastIndex à 0 et que matchAll exige le flag global, et que le flag unicode fait compter des points de code à . et à un quantificateur là où le défaut compte des unités UTF-16. Laissez le flag global hors d’un prédicat réutilisable, parcourez les correspondances avec matchAll ou une boucle exec qui possède l’état, et consignez la source, les flags, le point d’entrée et le lastIndex avant et après pour qu’une réponse stockée se reproduise plutôt que de se lire comme un résultat pur.
Lecture suivante
Pour la couche texte sur laquelle un motif tourne souvent, lisez TextDecoder et TextEncoder ; pour les vues binaires qu’un motif peut parcourir, lisez ArrayBuffer et TypedArray ; pour l’ordre qu’un sérialiseur donne aux clés de l’objet qu’un motif a capté, lisez Ordre des clés.
Cet article fait partie du guide : Qu'est-ce qu'une empreinte de navigateur ?
Articles Connexes
Unicode : quatre identités de texte lues par une page
EmpreintestructuredClone face à JSON : copies profondes
EmpreinteSource aléatoire de crypto-js : quelle branche
Empreinteua-parser-js : Client Hints, bots et un plafond de 500
EmpreinteQu'est-ce qu'une empreinte de navigateur ?
IdentitéHistorique de navigation injecté : session et global
Faites passer BotBrowser de la recherche à la production
Utilisez ces guides pour comprendre le modèle, puis passez à la validation multi-plateforme, aux contextes isolés et au déploiement navigateur prêt pour l'échelle.