Reference Cards
Reference Cards

Regex basics in JavaScript

Lesson · ~25 min
After this unit you can
  • Read classes ([^…], \d \w \s), quantifiers, anchors (^ $ \b), groups and alternation, and predict what a greedy vs a lazy quantifier matches
  • Choose the flag a job needs, telling m from s and knowing when text needs u (or v)
  • Pick test, match, matchAll or replace (with $1 and $<name>), and predict what match and replace return with and without g
  • Explain and fix the lastIndex trap of a g regex reused with test
  • Escape user input with RegExp.escape (ES2025) before building a regex from it

Cards

Character classes — [abc], [^abc], \d \w \s and .

Flag

A class matches one character from a set.

SyntaxMatches one…
[aeiou], [a-z0-9]listed character, or one in a range
[^0-9]character not in the set (^ first inside the brackets)
\d / \Ddigit [0-9] / anything else
\w / \Wword character [A-Za-z0-9_] / anything else
\s / \Swhitespace, including tabs, newlines and Unicode spaces / anything else
.character except a line terminator (\n, \r, \u2028, \u2029), unless the s flag is on

\d and \w are ASCII, even with u: /\w/u.test("é") and /\d/u.test("٣") are false. (With iu or iv, \w also matches ſ and the Kelvin sign U+212A.) For any script, use /\p{L}/u or /\p{Nd}/u.

Inside a class most metacharacters are literal — [.] matches a dot.

Quantifiers — greedy by default, lazy with ?

Flag

* (0 or more), + (1 or more), ? (0 or 1), {3} (exactly 3), {2,} (2 or more) and {2,4} (2 to 4) apply to the item just before them: a character, a class or a group.

Quantifiers are greedy — they take as much as they can and give back only what the rest of the pattern needs. A ? after one makes it lazy: as little as possible.

const html = "<b>bold</b> and <i>it</i>";
html.match(/<.+>/)[0];    // "<b>bold</b> and <i>it</i>" — first < to the last >
html.match(/<.+?>/)[0];   // "<b>"
html.match(/<[^>]+>/)[0]; // "<b>" — a negated class can't run past ">" at all

The negated class often states the intent better than laziness does. (For real HTML, use a parser, not a regex.)

Anchors — ^, $ and \b match positions, not characters

Flag
  • ^ is the start of the input and $ the end. With the m flag, they also match at the start and end of each line.
  • \b is a word boundary: a \w character on one side, and a non-\w character or the edge of the input on the other — so /\b!/.test("!hi") is false. \B is every other position.

Validation needs both anchors; without them the pattern only has to appear somewhere:

/\d{5}/.test("123456");         // true — "12345" is in there
/^\d{5}$/.test("123456");       // false
/\bcat\b/.test("concatenate");  // false
/\bcat\b/.test("the cat sat");  // true

Without m, JavaScript's $ matches only at the very end, not before a trailing newline: /^\d{5}$/.test("12345\n") is false.

Groups — capturing, non-capturing and named

Flag
SyntaxDoes
(abc)groups and captures — m[1], $1
(?:abc)groups only, for a quantifier or alternation; nothing captured
(?<year>\d{4})captures under a name — m.groups.year, $<year>
\1, \k<year>inside the pattern: the same text as that group, again
const m = "2026-10-17".match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/);
m.groups.year; // "2026"
m[1];          // "2026" — named groups are numbered too
m.index;       // 0

Names survive edits: adding a group earlier in the pattern renumbers $1, $2… but not the names. Reach for (?:…) whenever you need grouping but not the value, as in /^(?:https?|ftp):/.

Alternation — | has the lowest precedence

Flag

a|b matches either side, and | binds more loosely than anything else, so it splits the whole pattern unless a group holds it:

/^cat|dog$/.test("hotdog");      // true — means (^cat) or (dog$)
/^(?:cat|dog)$/.test("hotdog");  // false — exactly "cat" or exactly "dog"
"ab".match(/a|ab/)[0];           // "a" — the first alternative that works wins

The engine tries alternatives left to right and keeps the first one that leads to a match, not the longest. When one option is a prefix of another, put the longer one first: /ab|a/.

Flags — g i m s u v y d

Flag
FlagPropertyEffect
gglobalfind every match; makes test/exec stateful via lastIndex
iignoreCasecase-insensitive
mmultiline^ and $ also match at each line's start and end
sdotAll. also matches line terminators
uunicodeworks in code points; enables \p{…} and \u{…}
vunicodeSetsupgraded u (ES2024): set operations in classes, stricter syntax
ystickymatch only exactly at lastIndex
dhasIndicesadd start/end indices to each match (ES2022)

The classic mix-up: m doesn't let . cross lines — s does. u and v can't be combined (SyntaxError); v works in Node 20+ and every current browser.

u and v — see characters, not UTF-16 halves

Flag

Without u or v, a regex works on UTF-16 code units, so an emoji is two "characters":

"😀".length;                    // 2
/^.$/.test("😀");               // false — . matched only half of it
/^.$/u.test("😀");              // true
/\p{L}/u.test("é");             // true — a letter in any script
/\p{L}/.test("p{L}");           // true! without u, \p is just "p"
/^[\p{L}--[a-z]]$/v.test("é");  // true — v: letters minus a–z

Use u (or v) whenever the input is human text: . and quantifiers then work on whole code points, and \p{…} property escapes are available. v (ES2024) adds set operations inside classes — -- for difference, && for intersection — and properties of strings such as \p{RGI_Emoji}.

test, match, matchAll — what each one returns

Flag
const s = "2026-10-17";
/\d/.test(s);       // true — a boolean, nothing more
s.match(/(\d+)-/);  // ["2026-", "2026"] plus index, input, groups — first match only
s.match(/(\d+)-/g); // ["2026-", "10-"] — every match, but no groups or index
"abc".match(/\d/g); // null, not [] — guard before reading .length
[...s.matchAll(/(?<n>\d+)/g)].map((m) => m.groups.n); // ["2026", "10", "17"]
  • test answers yes or no.
  • match without g returns the first match with its groups; with g, only the matched strings.
  • matchAll yields every match with its groups and index, as an iterator. It requires the g flag and throws a TypeError without it.

replace — one match or all, $1, $<name>, or a function

Flag

replace changes one match unless the regex has g, and a string pattern is always replaced once. replaceAll replaces every occurrence (and throws a TypeError given a regex without g).

"a-b-c".replace("-", "+");                             // "a+b-c"
"a-b-c".replace(/-/g, "+");                            // "a+b+c"
"2026-10-17".replace(/(\d+)-(\d+)-(\d+)/, "$3/$2/$1"); // "17/10/2026"
"2026-10-17".replace(/(?<y>\d+)-(?<m>\d+)-(?<d>\d+)/, "$<d>/$<m>/$<y>"); // same
"10 and 25".replace(/\d+/g, (n) => String(n * 2));     // "20 and 50"

In a replacement string, $& is the whole match and $$ a literal $. A replacer function gets the match, then each group, the offset, the whole string, and a groups object when the pattern has named groups. Write "$<d>", not a template literal: ${d} is evaluated before replace ever runs.

split with a regex — flexible separators, and groups that sneak in

Flag

A regex separator handles messy input that a string separator can't:

"a, b ,c".split(/\s*,\s*/); // ["a", "b", "c"]
"a1b2c".split(/\d/);        // ["a", "b", "c"]
"a1b2c".split(/(\d)/);      // ["a", "1", "b", "2", "c"] — captured groups are spliced in
"a,b,c".split(",", 2);      // ["a", "b"] — the limit caps the number of entries

When the separator has a capturing group, each captured value lands in the result between the pieces. If you only need the group for alternation, make it non-capturing: /\s*(?:,|;)\s*/.

The lastIndex trap — a g regex remembers where it stopped

Flag

A regex with g (or y) is stateful. test and exec start searching at re.lastIndex, a match moves it forward — even if the next call passes a different string — and a miss resets it to 0.

const startsWithA = /^a/g;
["apple", "avocado", "banana", "apricot"].filter((w) => startsWithA.test(w));
// ["apple", "apricot"] — "avocado" was searched from index 1

The fix is to drop the g: test never needs it. If a shared regex must stay global, set re.lastIndex = 0 before each test, or create the regex inside the function.

match and replace reset a global regex's lastIndex to 0 before they start, and split ignores it. matchAll copies it, so a regex left mid-string by test makes matchAll skip matches too.

Escaping user input — RegExp.escape (ES2025)

Flag

new RegExp(input) treats the user's text as a pattern: . matches anything, + repeats, and input like ( or c++ throws a SyntaxError. Escape it first:

const input = "1.5+";
new RegExp(input).test("125");                  // true — "." and "+" were syntax
new RegExp(RegExp.escape(input)).test("125");   // false
new RegExp(RegExp.escape(input)).test("v1.5+"); // true
RegExp.escape(input);                           // the string \x31\.5\+

RegExp.escape is part of ES2025 and ships in Chrome 136, Firefox 134, Safari 18.2 and Node 24 (MDN: Baseline 2025). It also escapes a leading digit or letter, as \x31 above. On older runtimes, MDN points to a polyfill (core-js or es-shims) and advises against a hand-rolled replace, which misses edge cases.

Quiz

Which pattern accepts "12345" but rejects both "123456" and "zip 12345"?

Flag

Which strings does /^cat|dog$/ match?

Flag

/BEGIN.*END/.test("BEGIN\nbody\nEND") returns false. Which flag makes it true?

Flag

/^.$/.test("😀") returns false. Why, and what makes it true?

Flag

From "a=1&b=two&c=3" you need every pair, each with its key and value groups. Which call gives you that?

Flag

With s = "2026-10-17" and p = /(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/, which calls produce "17/10/2026"? Select all that work.

Flag

A module has const SLUG = /^[a-z0-9-]+$/g; and export const isSlug = (s) => SLUG.test(s);. isSlug("post-1") returns true, then isSlug("post-2") returns false. Why?

Flag

A search box runs new RegExp(query, "i") on whatever the user typed. Someone searches for c++. What happens, and what's the fix?

Flag

Predict the output

const re = /a/g;
console.log(re.test("a"));
console.log(re.test("a"));
console.log(re.test("a"));
Flag
const html = "<b>bold</b> and <i>it</i>";
console.log(html.match(/<.+>/)[0]);
console.log(html.match(/<.+?>/)[0]);
Flag
console.log("a-b-c".replace("-", "+"));
console.log("a-b-c".replace(/-/, "+"));
console.log("a-b-c".replace(/-/g, "+"));
Flag
const s = "2026-10-17";
const one = s.match(/(\d+)-/);
const all = s.match(/(\d+)-/g);
console.log(one[1], all[1]);
Flag

Further reading