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
gori. - 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.
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: seven02See 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.
03Pull out the parts
Capture groups remember pieces. Named groups make the code readable: no counting brackets.
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.
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.
const shout = "make it loud".replace(/\b\w/g, (letter) => letter.toUpperCase());
console.log(shout); // Make It LoudTry it yourself
- 1
Change the phone rule
In the first example, type a number with a
+91prefix into the input'svalue. Predict whether it passes. Then change the regex to accept an optional91at the start:/^(91)?\d{10}$/. - 2
Greedy vs lazy
In the first example, try
/<.*>/and/<.*?>/on"<i>a</i><b>b</b>". Predict both matches before you run. - 3
Swap to month names
In the second example, use a replacer function with
date.replace(/(\d+)-(\d+)-(\d+)/, (all, y, m, d) => ...)to return15 Jan 2024. Keep an array of month names and useNumber(m) - 1as its index.
Code & diagrams
`\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
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 trueThe `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
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 100A 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
Console
Nothing logged yet.Expected console output
true 2
false 0
true 2
true true true
true false trueBreak 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;.
Break #2
Forget to double backslashes in new RegExp
Write const re = new RegExp("^\d+$"); console.log(re.test("123"));.
Break #3
Search for user text that contains special characters
Build new RegExp(search) where the user typed C++.
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 aseslint-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.replacewith 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
A regular expression is a pattern for text. In JavaScript you usually write a regex literal between slashes:
/cat/matches the letterscatanywhere in a string. After the closing slash come flags that change how it searches:/cat/gi. You can also build one from a string withnew 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
Patterns are made of literal characters (they match themselves) and special characters. The most used:
.any one character except a line break;\da digit,\wa "word" character (letter, digit or_),\swhitespace, 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,\ba word boundary. To match a special character itself, escape it with a backslash:\.is a real dot,\$a real dollar. - 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
Groups. Parentheses
( )group parts together (so(ha)+matcheshahaha) 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 frommatch.groups.year.(?: )groups without capturing. Inreplace, the replacement text can use$1or$<year>to insert a capture, which makes reformatting easy:"2024-01-15".replace(/(\d+)-(\d+)-(\d+)/, "$3/$2/$1")gives"15/01/2024". - 5
Flags.
gglobal: find all matches, not just the first.iignore case.mmultiline:^and$match at every line start and end.sdotAll (ES2018):.also matches line breaks.uunicode (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 upgradedu.ysticky: match only exactly atlastIndex.d(ES2022): record start and end indices of every group. - 6
The methods.
regex.test(str)returnstrue/false.str.match(regex)withoutgreturns the first match as an array ([0]is the whole match,[1]... the groups, plus.indexand.groups), or **null** when nothing matches. Withgit returns an array of all matched strings (no groups) ornull.str.matchAll(regex)(ES2020, needsg) gives every match with its groups and index, ready forfor...of.str.replace(regex, x)replaces the first match, or all withg;xcan be a function that receives the match and its groups.str.split(regex)splits on a pattern, andstr.search(regex)returns the index of the first match or-1.
Explain it without notes
What is the difference between test, match and matchAll?
What does the g flag change, and why can it make test give different answers?
What are greedy and lazy quantifiers? Give an example.
How do you safely build a regex from user input?
Practice
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.
Write extractHashtags(text) that returns an array of hashtag words without the #, so extractHashtags("Learning #js and #CSS!") returns [ 'js', 'CSS' ].
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
DOMParserorJSON.parsefor 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
matchreturnsnullwhen nothing matches.I can find every match with
matchAll, and I avoidgon regexes used withtest.