Le secret caché des Regex en JavaScript qui rend fou !

Attention, une fois que vous aurez lu l’article, vous n’en ressortirez pas indemne !

Article publié le 23/10/2023, dernière mise à jour le 23/10/2023

Si vous jouez avec les expressions régulières en JavaScript, il se peut que vous ayez rencontré ce problème, à première vue incompréhensible :

const regex = /hello/g;
const text = 'hello world';

regex.exec(text); // => [ 'hello', index: 0, input: 'hello world', groups: undefined ]
regex.exec(text); // => null

À première vue, ça parait déjà bizarre…

Et pourtant, ce code-là, lui, fonctionne parfaitement :

const text = 'hello world';

/hello/g.exec(text); // => [ 'hello', index: 0, input: 'hello world', groups: undefined ]
/hello/g.exec(text); // => [ 'hello', index: 0, input: 'hello world', groups: undefined ]

D’où vient le problème ?

La syntaxe des expressions régulières en JavaScript fait que l’on oublie trop souvent qu’en réalité, ce code :

const regex = /hello/g;

est strictement égal à celui-ci :

const regex = new RegExp(/hello/,'g');

Même lorsque qu’elle est déclarée en utilisant la syntaxe raccourcie, une expression régulière est un objet complexe !

Et la deuxième spécificité, c’est que le fonctionnement de cet objet n’est pas le même en fonction des “flags” (paramètres) qui lui sont transmis…

Le flag “g”

Ici, on utilise le flag “g” pour “global”, ce qui signifie que l’expression régulière est censée détecter toutes les occurrences du modèle défini dans la Regex, au lieu de s’arrêter à la première.

Exemple :

const str = "hello hello hello world";

// Sans flag
str.replace(/hello/, "bonjour"); // => "bonjour hello hello world"
// Avec flag
str.replace(/hello/g, "bonjour"); // => "bonjour bonjour bonjour world"

Mais l’un effets de bord de l’utilisation du paramètre “g” d’une expression régulière, lorsque l’on appelle sa méthode .exec(…), est non pas de retourner l’ensemble des groupes détecté, mais de retourner le premier élément détecté, et de garder la position du dernier “match” en mémoire.

L’attribut en question s’appelle “lastIndex”

const regex = /hello/g;
const text = 'hello world';

console.log(regex.lastIndex); // => 0
regex.exec(text);
console.log(regex.lastIndex); // => 5
regex.exec(text);

La solution

Si ce comportement parait étrange à première vue, il permet néanmoins de gérer les détections globales avec une simple boucle while, comme ceci :

let str = 'hello hello hello world';
let regex = /hello/g;
let currentMatch = regex.exec(str);

while(currentMatch !== null) {
  console.log(currentMatch);
  currentMatch = regex.exec(string);
}

Voilà la seule et unique raison d’exister de ce fameux “lastIndex”, qui rend certaines expressions régulières en JavaScript “stateful”.

Un “hack”

Dans le cas où vous ne souhaiteriez pas utiliser votre RegEx dans une boucle, mais simplement l’utiliser sur des chaînes de caractères différentes, alors vous pouvez simplement remettre cet index à 0 :

const regex = /hello/g;
const text1 = 'hello world';
const text2 = 'hello you';

const match1 = regex.exec(text1);
regex.lastIndex = 0;
const match2 = regex.exec(text2);
regex.lastIndex = 0;
// ...

Vous avez terminé l'article ?

Commentaires (0)

pour laisser un commentaire

Aucun commentaire pour l'instant