Command Palette

Search for a command to run...

Hectal
PHASE 4Beginner ~26 min· topic 3 of 6

Topic 4.3

Regular Expressions Basics

In one line

A regular expression (regex) is a small pattern language for describing text, like "three digits, a dash, then four digits". You write it as /pattern/flags and use it with test, match, matchAll, replace and split to check, find, extract and rewrite text.

Think of it like this

A wanted poster for text. Instead of one exact word, the poster describes what to look for: "a capital letter, then some small letters, then a full stop". The detective (the regex engine) walks along the text from left to right, and at each spot asks "does what's here fit the poster?". Some posters say "find the first one", others say "find them all" (that's the g flag).

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Regular expression (regex)
A pattern that describes text, used to check, find or replace it.
Regex literal
A regex written between slashes in code, like /\d+/g.
Flag
A letter after the closing slash that changes how the search works, like g or i.
Character class
A set of characters, any one of which can match, like [aeiou] or \d.
Anchor
A pattern that matches a position, such as the start ^, the end $ or a word edge \b.
Quantifier
Says how many times the thing before it may repeat: *, +, ? or {n,m}.
Greedy
A quantifier that grabs as many characters as possible. Adding ? makes it lazy.
Capture group
Part of a pattern in parentheses whose matched text is remembered and can be read later.
Escape
Put a backslash before a special character so it matches itself, like \. for a real dot.

Step by step

01Ask a yes-or-no question

Does this look like an Indian PIN code (exactly six digits)? ^ and $ pin the pattern to the start and end, so nothing else may be around the digits.

regex.jswhole filejavascript
const pin = /^\d{6}$/;
console.log(pin.test("560001"));   // true
console.log(pin.test("56000"));    // false: only five digits
console.log(pin.test("5600012"));  // false: seven

02See how the engine matches

The engine tries the pattern at position 0. If it fails, it moves one character on and tries again. Inside a try, a greedy quantifier takes as much as it can, then backtracks (gives characters back one at a time) if the rest of the pattern can't match.

See how the engine matchesdiagram
Rendering diagram…

03Pull out the parts

Capture groups remember pieces. Named groups make the code readable: no counting brackets.

regex.jswhole filejavascript
const m = "Order 2024-01-15".match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/);
console.log(m[0]);           // 2024-01-15 (the whole match)
console.log(m.groups.month); // 01
console.log(m.index);        // 6 (where it starts)

04Find every match

matchAll needs the g flag and returns an iterator: each item is a full match object with groups and index.

regex.jswhole filejavascript
const text = "#js and #css and #html";
for (const m of text.matchAll(/#(\w+)/g)) {
  console.log(m[1], "at", m.index);
}

05Rewrite with replace

With the g flag, replace changes every match. Here a function decides each replacement: it receives the whole match first, then each group.

regex.jswhole filejavascript
const shout = "make it loud".replace(/\b\w/g, (letter) => letter.toUpperCase());
console.log(shout); // Make It Loud

Try it yourself

  1. 1

    Change the phone rule

    In the first example, type a number with a +91 prefix into the input's value. Predict whether it passes. Then change the regex to accept an optional 91 at the start: /^(91)?\d{10}$/.

  2. 2

    Greedy vs lazy

    In the first example, try /<.*>/ and /<.*?>/ on "<i>a</i><b>b</b>". Predict both matches before you run.

  3. 3

    Swap to month names

    In the second example, use a replacer function with date.replace(/(\d+)-(\d+)-(\d+)/, (all, y, m, d) => ...) to return 15 Jan 2024. Keep an array of month names and use Number(m) - 1 as its index.

Code & diagrams

test, match and the flags New tab

`\D` (not a digit) with `g` strips every space and dash in one go. `match` without `g` returns the first match (the console shows its items; it also has `.index` and `.groups`). `exec` is the regex-side version of `match` without `g`.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

9876543210 true
[ 'cats', 'Cats' ]
null
[ '1', '22', '333' ]
[ '1' ]
<b>hi</b> <b>
true false
[ 'one', 'two', 'three' ]
false true
Groups, matchAll and replace ES2020+ New tab

The `groups` object has no prototype, which is why the console labels it `null prototype`; you read it like any object. The replacer function's return value (a number here) is turned into a string.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

2024 01 15 0
[Object: null prototype] { year: '2024', month: '01', day: '15' }
15.2024
15/01/2024
15/01/2024
total 95
tea 60, samosa 30, juice 100
The lastIndex trap with g and test New tab

A regex with `g` (or `y`) remembers where the last match ended in `lastIndex` and starts the next search there. For yes-or-no checks, leave out `g`.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

true 2
false 0
true 2
true true true
true false true

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Use the result of match without checking

Write const count = "no digits here".match(/\d/g).length;.

terminal
$ Console
── what you'll see ──
Uncaught TypeError: Cannot read properties of null (reading 'length')

Break #2

Forget to double backslashes in new RegExp

Write const re = new RegExp("^\d+$"); console.log(re.test("123"));.

terminal
$ Console
── what you'll see ──
false

Break #3

Search for user text that contains special characters

Build new RegExp(search) where the user typed C++.

terminal
$ Console
── what you'll see ──
Uncaught SyntaxError: Invalid regular expression: /C++/: Nothing to repeat

Myth vs fact

Myth

You need a regex for every text check.

Fact

includes, startsWith, split and trim cover many cases and are easier to read. Use a regex when there is a real pattern.

Myth

A good regex can fully validate an email address.

Fact

The full email grammar is huge, and a strict regex rejects real addresses. Check for a simple shape (/^[^\s@]+@[^\s@]+\.[^\s@]+$/), use <input type="email"> (see /html), and confirm by sending an email.

Myth

\w matches any letter.

Fact

\w is only [A-Za-z0-9_]. It doesn't match é or न. Use \p{L} with the u flag for letters in every language.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    JavaScript's engine is a backtracking engine (V8 uses Irregexp). Patterns with nested quantifiers like (a+)+$ or overlapping alternatives can take exponential time on some inputs, freezing the page or a Node server: this is called ReDoS (regular expression denial of service). Defend by avoiding nested quantifiers over the same characters, limiting input length before matching, and testing patterns with linting tools such as eslint-plugin-regexp. V8 also has an optional non-backtracking mode for some patterns behind a flag.

  • ▸

    Regex literals are compiled once when the code is parsed and reused, so defining one inside a loop is cheap. new RegExp(...) in a hot loop compiles each time (engines cache some of this); build it once outside the loop.

  • ▸

    Lookarounds match a position by checking what's around it without consuming it: (?=...) lookahead, (?!...) negative lookahead, (?<=...) lookbehind and (?<!...) negative lookbehind (ES2018). /\d+(?=%)/ finds a number followed by % without including the %.

  • ▸

    String.prototype.replace with a regex and $ patterns is handy, but when replacement text comes from users, use a replacer function, so a $ in their text is inserted literally.

Remember this

  1. 1

    A regular expression is a pattern for text. In JavaScript you usually write a regex literal between slashes: /cat/ matches the letters cat anywhere in a string. After the closing slash come flags that change how it searches: /cat/gi. You can also build one from a string with new RegExp("cat", "gi"), which is useful when the pattern comes from a variable (but then every backslash must be doubled in the string: new RegExp("\\d+")).

  2. 2

    Patterns are made of literal characters (they match themselves) and special characters. The most used: . any one character except a line break; \d a digit, \w a "word" character (letter, digit or _), \s whitespace, and their opposites \D, \W, \S; [abc] one of these characters, [a-z] a range, [^0-9] anything except these. Anchors match positions, not characters: ^ start, $ end, \b a word boundary. To match a special character itself, escape it with a backslash: \. is a real dot, \$ a real dollar.

  3. 3

    Quantifiers say how many: * zero or more, + one or more, ? zero or one (optional), {3} exactly three, {2,4} two to four, {2,} two or more. They are greedy: they take as much as they can and give back only if the rest fails. Add ? to make them lazy (as little as possible): on <b>hi</b>, /<.+>/ matches the whole thing, /<.+?>/ matches only <b>. | means "or": /cat|dog/.

  4. 4

    Groups. Parentheses ( ) group parts together (so (ha)+ matches hahaha) and capture what they matched, numbered from 1 in the order of their opening brackets. Named groups (?<year>\d{4}) (ES2018) give captures a name you read from match.groups.year. (?: ) groups without capturing. In replace, the replacement text can use $1 or $<year> to insert a capture, which makes reformatting easy: "2024-01-15".replace(/(\d+)-(\d+)-(\d+)/, "$3/$2/$1") gives "15/01/2024".

  5. 5

    Flags. g global: find all matches, not just the first. i ignore case. m multiline: ^ and $ match at every line start and end. s dotAll (ES2018): . also matches line breaks. u unicode (ES2015): treat surrogate pairs as one character and allow \p{...} property escapes such as \p{L} (any letter in any language); v (ES2024) is an upgraded u. y sticky: match only exactly at lastIndex. d (ES2022): record start and end indices of every group.

  6. 6

    The methods. regex.test(str) returns true/false. str.match(regex) without g returns the first match as an array ([0] is the whole match, [1]... the groups, plus .index and .groups), or **null** when nothing matches. With g it returns an array of all matched strings (no groups) or null. str.matchAll(regex) (ES2020, needs g) gives every match with its groups and index, ready for for...of. str.replace(regex, x) replaces the first match, or all with g; x can be a function that receives the match and its groups. str.split(regex) splits on a pattern, and str.search(regex) returns the index of the first match or -1.

Explain it without notes

01

What is the difference between test, match and matchAll?

02

What does the g flag change, and why can it make test give different answers?

03

What are greedy and lazy quantifiers? Give an example.

04

How do you safely build a regex from user input?

Practice

01

Write isStrongPassword(p) that returns true when p has at least 8 characters, at least one digit, one lower-case and one upper-case letter. Use several small regexes rather than one giant one.

02

Write extractHashtags(text) that returns an array of hashtag words without the #, so extractHashtags("Learning #js and #CSS!") returns [ 'js', 'CSS' ].

03

Write camelToKebab(name) that turns "backgroundColor" into "background-color".

Trade-offs

  • ↔

    One clever regex is compact but hard to read and debug; several small ones (or plain string methods) are longer but clearer and give better error messages.

  • ↔

    Regex is great for patterns in flat text, but the wrong tool for nested formats like HTML or JSON: use DOMParser or JSON.parse for those.

Done when you can

  • I can write a regex literal with flags and test it with test.

  • I can use character classes, anchors and quantifiers, and escape special characters.

  • I can capture parts with numbered and named groups and use them in replace.

  • I know match returns null when nothing matches.

  • I can find every match with matchAll, and I avoid g on regexes used with test.