SKU // 93

JavaScript RegExp Objects

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.

// A Regex Is an Object

const re = /\d+/g;

typeof re                 // "object"
re instanceof RegExp        // true

// Properties

const re = /ab+c/gi;
Property Value Meaning
source"ab+c"The pattern text
flags"gi"All flags as a string
global / ignoreCase / multilinetrue / true / falseOne boolean per flag
lastIndex0Where the next search starts (g or y only)

// test(): The Quick Yes or No

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!"));
true true false

// lastIndex: The Hidden State

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
1 2 22 6 333 11 null 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(): The Detailed Match

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"
ava@site.com ava site 5 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]);
}
a 1 b 2
Watch out
That loop only ends because g moves lastIndex forward. Without g, exec finds the first match every time and the loop never stops.

// Building Patterns Dynamically

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");
[Java] is not JavaScript. [java]!

Remember to double the backslashes in the string (\\b), and escape user-supplied words with escapeRegExp from the Metacharacters lesson.

// RegExp.escape() (New in 2025)

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"));
\[\*\] W3Schools is a web school. [W3Schools] is a web school.

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.

W3Schools error
W3Schools' RegExp.escape() example is missing a closing parenthesis: const safe = RegExp.escape("[*]"; is SyntaxError: missing ) after argument list, so the example never runs.

// Copying and Changing Flags

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"

// Comparing Regexes

/a/ === /a/                          // false  (two different objects)
String(/a/g) === String(/a/g)        // true   ("/a/g" both)

// Quick Check

What does this log?

const re = /\d/g;
console.log(re.test("a1"), re.lastIndex);
console.log(re.test("b2"), re.lastIndex);
Show answer
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?

  1. "/cat/gi"
  2. "cat"
  3. "gi"
  4. ["c", "a", "t"]
Show answer
B. 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")?

Show answer
The word may contain characters like . * ( [ that have special meanings. Unescaped, they change what matches or throw a SyntaxError (try "C++"). Escaping makes every character literal.

// Try It

  1. Click Open Playground. Log source, flags, and global for three different regexes
  2. Call exec repeatedly on a g regex and watch lastIndex change
  3. Write countWord(text, word) that builds a regex with new RegExp and counts whole-word matches
  4. Take /dog/ and create a case-insensitive global copy of it
// Video Walkthroughs
Last updated: Oct 10, 2026, 1:47 AM ET