Skip to content

06 · Regular Expressions in JS

RegExp literals

A regular expression is a pattern for matching text. JavaScript has a built-in literal syntax using slashes, plus flags that change how the pattern behaves.

const hasDigit = /\d/;       // matches any single digit
const wordChars = /\w+/;      // matches one or more word characters
const caseInsensitive = /hello/i; // 'i' flag ignores case
const global = /a/g;           // 'g' flag finds ALL matches, not just the first

console.log(hasDigit.test("abc123")); // true
console.log(hasDigit.test("abc"));    // false
console.log(caseInsensitive.test("HELLO world")); // true

Common pattern building blocks

Syntax Meaning
\d a digit (0-9)
\w a word character (letters, digits, underscore)
\s whitespace (space, tab, newline)
. any character except a newline
+ one or more of the previous token
* zero or more of the previous token
? zero or one of the previous token (also marks "lazy" matching)
^ / $ start / end of the string (or line, with the m flag)
[abc] any one of a, b, or c
(...) a capture group
{2,4} between 2 and 4 repetitions
const zipCode = /^\d{5}$/;          // exactly 5 digits, whole string
const simpleEmail = /^[\w.-]+@[\w.-]+\.\w+$/; // basic (not RFC-complete) email shape

console.log(zipCode.test("94107"));      // true
console.log(zipCode.test("9410"));       // false — only 4 digits
console.log(simpleEmail.test("ada@example.com")); // true
console.log(simpleEmail.test("not-an-email"));    // false

test and exec

.test() returns a boolean. .exec() returns match details (or null), and with the g flag can be called repeatedly to step through all matches.

const pattern = /\d+/g;
const text = "Order 12 has 3 items, order 45 has 7 items";

let match;
while ((match = pattern.exec(text)) !== null) {
  console.log(match[0], "at index", match.index);
}
// 12 at index 6
// 3 at index 13
// 45 at index 30
// 7 at index 40

String methods that accept a RegExp: match, matchAll, replace

Strings themselves expose regex-powered methods, often more convenient than exec for one-off extraction or substitution.

const sentence = "cats, dogs, and cats again";

console.log(sentence.match(/cats/));    // first match only (no 'g' flag): ['cats', index: 0, ...]
console.log(sentence.match(/cats/g));   // ['cats', 'cats'] — every match, as plain strings

console.log(sentence.replace("cats", "birds"));  // string arg replaces only the first occurrence
console.log(sentence.replace(/cats/g, "birds")); // regex + 'g' replaces every occurrence
// birds, dogs, and birds again

const allMatches = [...sentence.matchAll(/cats/g)]; // full match objects, spreadable
console.log(allMatches.length); // 2

Capture groups

Parentheses create capture groups, letting you pull specific pieces out of a match instead of the whole thing — named groups make the result even more readable.

const logLine = "2024-03-15 ERROR: disk full";
const logPattern = /^(\d{4})-(\d{2})-(\d{2}) (\w+): (.+)$/;

const [, year, month, day, level, message] = logLine.match(logPattern);
console.log(year, month, day, level, message);
// 2024 03 15 ERROR disk full
// Named capture groups — clearer than positional destructuring
const namedPattern = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const result = "2024-03-15 ERROR: disk full".match(namedPattern);

console.log(result.groups.year);  // 2024
console.log(result.groups.month); // 03

Using capture groups in replace

Reference captured groups inside the replacement string with $1, $2, etc. — useful for reformatting text without manually rebuilding it.

const rawDate = "15/03/2024"; // DD/MM/YYYY
const isoDate = rawDate.replace(/(\d{2})\/(\d{2})\/(\d{4})/, "$3-$2-$1");
console.log(isoDate); // 2024-03-15

test/exec/match/replace cheat sheet

Method Called on Returns Best for
regex.test(str) RegExp boolean quick yes/no validation
regex.exec(str) RegExp match array or null, stateful with g stepping through matches one at a time
str.match(regex) String first match, or all matches (with g) extracting matched text
str.matchAll(regex) String iterator of full match objects (requires g) extracting matches with capture groups
str.replace(regex, ...) String new string substituting or reformatting matched text

How It Actually Works

V8's regex engine (Irregexp) compiles each pattern into actual native machine code the first time it's used — not an interpreted state machine — because regex matching is common enough in real workloads to justify JIT compilation just like function bodies get. Simple patterns without backreferences or complex lookaround compile to a genuine finite automaton with linear-time matching guarantees. But JavaScript regex also supports backreferences (\1) and lookahead/lookbehind, which cannot be expressed as a pure finite automaton — Irregexp falls back to a backtracking engine for these, which is what makes certain patterns (nested quantifiers like (a+)+b against a non-matching string) exhibit catastrophic exponential-time blowup: the engine tries every possible way to split the input among the repeated groups before giving up.

The g and y flags matter beyond "match all": a regex object with g set is stateful — it stores a lastIndex property and mutates it after each .exec() call, resuming the next call from where the last match ended. This is why reusing the same global regex object across unrelated calls (e.g., storing one at module scope and calling .test() on different strings) produces bugs where matches are silently skipped: lastIndex from a previous call is still there, has nothing to do with the new string, and its residual value point past where a match would have been found.

Exercise

Write a function extractHashtags(text) that returns an array of all hashtags in a string (e.g. "loving #javascript and #webdev"["#javascript", "#webdev"]) using a regular expression with the global flag.