Every regex is an object: an instance of RegExp with properties you can read and one piece of state that changes as you use it. Knowing what is inside the object explains some of regex's stranger behavior and lets you build patterns on the fly.
const re = /\d+/g;
typeof re // "object"
re instanceof RegExp // true
const re = /ab+c/gi;
| Property | Value | Meaning |
|---|---|---|
| source | "ab+c" | The pattern text |
| flags | "gi" | All flags as a string |
| global / ignoreCase / multiline | true / true / false | One boolean per flag |
| lastIndex | 0 | Where the next search starts (g or y only) |
test() is a method of the regex itself. It searches a string and returns true or false. You do not need to store the regex in a variable first:
const pattern = /e/;
console.log(pattern.test("The best things in life are free!"));
console.log(/e/.test("The best things in life are free!")); // same, in one line
console.log(/z/.test("The best things in life are free!"));
A regex with g or y remembers where its last match ended. exec() and test() start from there next time:
const re = /\d+/g;
const s = "a1 b22 c333";
re.exec(s)[0]; re.lastIndex; // "1" 2
re.exec(s)[0]; re.lastIndex; // "22" 6
re.exec(s)[0]; re.lastIndex; // "333" 11
re.exec(s); // null, and lastIndex resets to 0
This is the reason for the g + test() flip-flop from the Flags lesson. You can reset it yourself with re.lastIndex = 0.
exec() returns one match at a time with full details, or null:
const m = /(\w+)@(\w+)\.com/.exec("Mail ava@site.com today");
m[0] // "ava@site.com"
m[1] // "ava"
m[2] // "site"
m.index // 5 where the match starts
m.input // "Mail ava@site.com today"
Before matchAll existed, looping with exec was the standard way to get every match with its groups. You will still see it in older code:
const re = /(\w)=(\d)/g;
let m;
while ((m = re.exec("a=1 b=2")) !== null) {
console.log(m[1], m[2]);
}
g moves lastIndex forward. Without g, exec finds the first match every time and the loop never stops.
The RegExp constructor builds a pattern from strings, which is how you search for something only known at run time:
function highlight(text, word) {
const re = new RegExp(`\\b${word}\\b`, "gi");
return text.replace(re, m => `[${m}]`);
}
highlight("Java is not JavaScript. java!", "java");
Remember to double the backslashes in the string (\\b), and escape user-supplied words with escapeRegExp from the Metacharacters lesson.
RegExp Metacharacters introduced RegExp.escape(). It takes any text and backslash-escapes every character that has a special meaning in a regex, so you can safely build a pattern from it:
const safe = RegExp.escape("[*]"); // escape the text
const regex = new RegExp(safe); // build the regex
console.log(safe);
console.log("[*] is a web school.".replace(regex, "W3Schools"));
// the same thing WITHOUT escaping
console.log("[*] is a web school.".replace(new RegExp("[*]"), "W3Schools"));
Without escaping, [*] is a character class that matches one *, so only the star is replaced. RegExp.escape is in every current browser (2025 and later). For older browsers, use the escapeRegExp function from RegExp Metacharacters.
RegExp.escape() example is missing a closing parenthesis: const safe = RegExp.escape("[*]"; is SyntaxError: missing ) after argument list, so the example never runs.
Apart from lastIndex, a regex's properties are read-only: re.global = false is silently ignored (and a TypeError in strict mode). To change flags, make a new one from the old:
const base = /cat/;
const global = new RegExp(base, "gi"); // same pattern, new flags
global.source // "cat"
global.flags // "gi"
/a/ === /a/ // false (two different objects)
String(/a/g) === String(/a/g) // true ("/a/g" both)
What does this log?
const re = /\d/g;
console.log(re.test("a1"), re.lastIndex);
console.log(re.test("b2"), re.lastIndex);
true 2, then false 0. After the first match, lastIndex is 2, so the second search starts at position 2 of "b2", which is past the "2". It fails and lastIndex resets to 0.What does /cat/gi.source return?
"/cat/gi""cat""gi"["c", "a", "t"]source is just the pattern text. flags gives "gi", and toString() gives "/cat/gi".A user types a search word. Why is new RegExp(RegExp.escape(word), "g") safer than new RegExp(word, "g")?
. * ( [ that have special meanings. Unescaped, they change what matches or throw a SyntaxError (try "C++"). Escaping makes every character literal.source, flags, and global for three different regexesexec repeatedly on a g regex and watch lastIndex changecountWord(text, word) that builds a regex with new RegExp and counts whole-word matches/dog/ and create a case-insensitive global copy of it