Course outline · 0% complete

0/27 lessons0%

Course overview →

Regex basics: test, classes, quantifiers

lesson 9-1 · ~14 min · 23/27

It evaluates to "", the empty string.

?? only falls back for null and undefined, and "" is neither of those.

Swapping in || would have produced "default" instead, which is the distinction from the previous lesson in one line.

Fresh topic now. Regular expressions are the pattern language for matching text, and they are the last major piece of everyday JavaScript this course has not covered.

They are also a fair interview topic in a narrow way. Nobody expects you to write a lookbehind from memory, and everybody expects you to know why /\d{5}/ accepts "zip: 90210!".

A pattern language for strings

A regular expression, or regex, describes a text pattern.

In JavaScript you write one between slashes, and the simplest use is pattern.test(string), which returns true or false.

Regex exists because "find text of this shape" written with plain string methods turns into unreadable loops of indexOf and slice.

It is the engine behind input validation, log searching, editor find-and-replace, and URL routing, with a few characters of pattern replacing dozens of lines of code.

Here is the core vocabulary.

PieceMatches
catthe literal letters c, a, t
\done digit, with \w for word characters and \s for whitespace
.any single character
x+one or more x, with x* for zero or more and x? for optional
x{5}exactly five x, with x{2,4} for two to four
[abc]one character from the set, and [^abc] for not in the set
^ and $start and end of the string

Uppercase negates the shorthand classes, so \D, \W, and \S match anything those classes do not.

Without anchors a regex matches anywhere inside the string, which is the number one source of validation bugs.

Anchors decide what "valid" means

The same five-digit rule, twice.

const zip = /^\d{5}$/;
console.log(zip.test("90210"));
console.log(zip.test("9021"));
console.log(zip.test("zip: 90210!"));

const sloppy = /\d{5}/;
console.log(sloppy.test("zip: 90210!"));

Output

true
false
false
true

The anchored pattern demands the whole string be five digits, and the unanchored version happily matches five digits hiding inside a longer string.

"9021" fails because {5} means exactly five, and there is no fifth digit to consume.

"zip: 90210!" is the case that matters. Both patterns find five digits in it, and only the anchored one rejects the surrounding text.

"902100" is worth thinking through as well. The anchored version rejects it, since $ cannot follow the fifth digit when a sixth exists.

The rule to internalize is that test asks "is this pattern present" and anchors turn that into "is this pattern the entire string". Validation almost always wants the second question.

/^\d{5}$/start of stringa digit 0–9exactly 5 of themend of string
Anatomy of /^\d{5}$/: both anchors force the whole string to be exactly five digits.

Yes, it accepts it, because an unanchored pattern matches "bob" anywhere inside the string.

test() looks for the pattern anywhere in the string, and "bob" satisfies \w+ on its own.

To validate the entire input you have to anchor it as /^\w+$/, which then rejects the exclamation marks.

The quantifier is not the problem, and that is the part worth noticing. \w+ is perfectly happy matching a three-character run inside a nine-character string, since nothing told it to cover everything.

The same bug appears in every unanchored validator. An email pattern without anchors accepts "attack<script>a@b.co", which is how regex validation becomes a security issue rather than a cosmetic one.

Anchoring mistakes are the classic regex interview gotcha, and the fix is mechanical. Any pattern used for validation gets ^ and $.

Alternation and escaping

Two more pieces complete the starter kit.

a|b is alternation, matching either side. Wrap it in parentheses to limit its reach, so /\.(png|jpe?g|gif)$/ means a literal dot, then one of three extensions, then end of string.

Alternation has the lowest precedence of anything in a pattern, which is exactly why those parentheses matter. /^cat|dog$/ means "starts with cat" or "ends with dog", not what it looks like.

Characters the pattern language reserves must be escaped with a backslash to be matched literally. The set is . + ? ( ) [ ] { } $ ^ | \ and the slash delimiter itself.

An unescaped . matches any character, so /one.png$/ happily accepts "onexpng", which is a real bug class in file-type checks.

Escaping is easy to forget with user-supplied text, and building a pattern from a search box is where it bites. new RegExp(userInput) can throw on an unbalanced bracket or match far more than intended.

RegExp.escape exists in newer runtimes for exactly that, and the durable habit is to avoid building patterns from untrusted strings at all.

An extension check

An escaped dot and a grouped alternation.

const image = /\.(png|jpe?g|gif)$/;
console.log(image.test("photo.jpg"));
console.log(image.test("photo.jpeg"));
console.log(image.test("notes.txt"));
console.log(image.test("trickypng"));

Output

true
true
false
false

The escaped \. demands a real dot, so trickypng fails, and the alternation accepts all three extensions, with jpe?g covering both jpg and jpeg.

e? is the optional quantifier applied to a single character, which is a tidier way to express two spellings than jpg|jpeg.

The $ anchor is doing real work here too. Without it, "photo.png.exe" would match, which is a genuine security-relevant mistake.

Only $ is needed rather than both anchors, because a filename check cares about the ending and not the beginning.

Case is not handled, so "PHOTO.JPG" fails. Adding the i flag as /\.(png|jpe?g|gif)$/i fixes it, and flags are the subject of the next lesson.

time24

A fixed-shape pattern with both anchors.

const time24 = /^\d{2}:\d{2}$/;

console.log(time24.test("09:45"));
console.log(time24.test("9:45"));
console.log(time24.test("almost 09:45"));

Output

true
false
false

Two digits is \d{2}, and the colon is just a literal : in the pattern, since it is not a reserved character.

"9:45" fails because {2} requires exactly two digits before the colon.

"almost 09:45" fails because of the anchors, and it would pass without them.

This pattern shape is what it claims to be and nothing more, so "99:99" passes. Rejecting that takes real ranges, roughly /^([01]\d|2[0-3]):[0-5]\d$/, which is a good illustration of how quickly validation patterns grow.

That growth is the practical lesson. Regex is excellent at shape and poor at meaning, so past a certain point parsing the parts and checking them as numbers is clearer than a longer pattern.