Chapter 7 · Expressions & Built-in Data

Regular Expressions

Regular expressions are a small language for describing text patterns. JavaScript's regex engine gained a lot since ES5: named groups, lookbehind, Unicode properties, matchAll, new flags and RegExp.escape.

You will learn

  • How to create a regex: literals vs the RegExp constructor, and escaping user input.
  • Character classes, quantifiers, anchors, groups and backreferences.
  • Named groups and lookbehind ES2018, Unicode property escapes.
  • The flags g i m s u y d v, and what each changes.
  • test, exec, match, matchAll ES2020, replace and split, plus the stateful lastIndex trap.

Creating a regular expression

⚠️ Pitfall: never put raw user input into new RegExp(input). Characters like ., + and ( have special meanings, and a malicious pattern can freeze your program (catastrophic backtracking). Use RegExp.escape ES2025. (Notice that it even escapes a leading letter, as \x61, so that the result is safe in any position.)

Pattern syntax

PatternMatches
.any character except line breaks (unless the s flag is set)
\d \w \s / \D \W \Sa digit, a word character [A-Za-z0-9_], whitespace / their opposites
[aeiou] [^,] [a-z]one character from a set / not in a set / a range
\p{L}, \p{Lu}, \p{Emoji}Unicode properties: any letter, an uppercase letter, an emoji (needs u or v)
* + ? {2} {2,5} {2,}0+, 1+, 0 or 1, exactly 2, 2 to 5, at least 2 times
*? +? ??lazy versions: match as little as possible
^ $ \b \Bstart, end, word boundary, not a boundary
(…) (?:…) (?<name>…)capturing group, non-capturing group, named group ES2018
\1 \k<name>backreference to a group's text
a|balternation
(?=…) (?!…)lookahead: followed by / not followed by
(?<=…) (?<!…)lookbehind: preceded by / not preceded by ES2018

The first of the last two examples is a classic surprise. The goal was "numbers not followed by ' qty'". 100 is followed by " qty", so the lookahead fails, and the engine backtracks: it tries the shorter 10, which is followed by 0, not " qty". So 10 matches, and you get #0. Adding \d to the negative lookahead, (?!\d| qty), forbids stopping in the middle of a number. Always test lookarounds on real data.

Named groups ES2018

Named groups make patterns self-documenting, and they show up on match.groups. In replace, refer to them with $<name>.

Flags

FlagNameEffect
gglobalfind all matches; makes exec/test stateful via lastIndex
iignoreCasecase-insensitive
mmultiline^ and $ match at every line start and end
sdotAll ES2018. also matches line breaks
uunicode ES2015works on code points, enables \p{…}, stricter syntax
ysticky ES2015match only at exactly lastIndex (useful for tokenizers)
dhasIndices ES2022adds match.indices with the start/end of each group
vunicodeSets ES2024an upgraded u: set operations in classes like [\p{L}--[a-z]]

Methods

MethodReturns
re.test(str)true / false
re.exec(str)one match array (with groups and index), or null
str.match(re)without g: like exec. With g: an array of the matched strings only, or null
str.matchAll(re) ES2020an iterator of full match objects; requires g
str.replace(re, x) / replaceAlla new string; x may be a string with $1/$<name>/$& or a function
str.split(re), str.search(re)an array of parts / the index of the first match

The lastIndex trap

A regex with g or y remembers where its last match ended, in lastIndex. Reusing the same regex object with test or exec continues from there, which leads to alternating results.

❌ A shared global regex in test()

✅ No g flag for yes/no checks

More examples

1. Validating a simple postcode

2. Extracting all prices from text

3. Slugifying a title with Unicode classes

4. Highlighting search terms safely

5. A tiny tokenizer with the sticky flag

Recap

  • Use literals for fixed patterns and new RegExp for dynamic ones; escape user input with RegExp.escape.
  • Quantifiers are greedy unless you add ?. Use \b for word boundaries, and lookarounds to check context without consuming it.
  • Named groups give readable match.groups, and $<name> in replacements.
  • Flags: g i m s u y d v. Use u or v for Unicode-correct matching.
  • Use matchAll for full details of every match. Never share a g regex for test() checks.

Exercises

Exercise 1 🟢 easy build it

Write a regex that tests for a 4-digit year from 1900 to 2099.

Show solution

The anchors ^ and $ make the whole string match, not just a part of it.

Exercise 2 🟢 easy predict the output

What do "a1b22c333".match(/\d+/g) and "a1b22c333".match(/\d+/)[0] return?

Show solution

[ '1', '22', '333' ] and '1'. With g, match returns every match; without it, only the first (plus group details).

Exercise 3 🟢 easy fix the bug

This should replace every dot with a dash, but it replaces everything. Fix it.

Show solution

An unescaped . means "any character". For a literal text replacement, replaceAll with a string avoids regex altogether.

Exercise 4 🟢 easy explain why

Why does /^\p{L}+$/.test("Émile") throw or misbehave without a flag, and what fixes it?

Show solution

Without u (or v), \p is just an escaped "p", so the pattern means "one or more p's followed by {L}", and it fails. Adding the u flag enables Unicode property escapes: /^\p{L}+$/u.

Exercise 5 🟡 medium build it

Using named groups, turn "Herbert, Frank" into "Frank Herbert" with a single replace.

Show solution

[^,]+ stops at the comma, and $<name> refers to named groups in the replacement string.

Exercise 6 🟡 medium predict the output

What does this print?

Show solution

true 2 true 3 false. Each test continues from lastIndex: it finds the "o" at index 1, then the one at index 2, then finds nothing and resets lastIndex to 0.

Exercise 7 🟡 medium build it · playground

Create playground/parse-orders.js exporting parseOrders(text), which reads lines like ORD-0001 | Ana | 2x b1, 1x b7 and returns { id, customer, items: [{ qty, book }] } objects, skipping lines that don't match.

Show solution

Lazy +? keeps the customer name from swallowing the trailing spaces. flatMap with [] drops the non-matching lines (Chapter 8).

Exercise 8 🟡 medium refactor it

Replace this exec loop with matchAll.

Show solution

matchAll returns an iterator of full match objects, without manual lastIndex bookkeeping.

Exercise 9 🔴 hard build it

Format an integer with thousands separators using a single replace with lookarounds: 1234567 → "1,234,567".

Show solution

It inserts a comma at every non-boundary position that is followed by a multiple of three digits up to the end. In real code, Intl.NumberFormat (Chapter 6) is the right tool.

Exercise 10 🔴 hard mini-project · playground

Create playground/validate.js exporting a validators object (email-ish, ISBN-13 with or without dashes, and a coupon code like SAVE-10 or SAVE-25), plus check(field, value), which returns { ok, value } with the normalized value (lower-cased email, ISBN without dashes, upper-cased coupon).

Show solution

Patterns without g are safe to share for test(). (?:-?\d){10} allows optional dashes between digits, and each validator pairs its pattern with a normalizer.

Official docs