Skip to main content
Adapted, not copied

Original notes for Javascript360.org β€” topic order inspired by Eloquent Javascript (4th ed.) by Marijn Haverbeke. See the full attribution note.

Chapter 9: Regular Expressions

Estimated time: ~45 minutes Β· Chapter type: Concept

What you'll learn​

  • How to build and test regular expressions
  • Character classes, quantifiers, and capturing groups
  • Greedy vs. lazy matching, and a common backtracking pitfall
  • A worked example: parsing a simple config file

Creating a regular expression​

A regular expression (regex) is a tiny, specialized language for describing a pattern of text, rather than one exact string. Javascript has a literal syntax, slashes on either side:

const hasDigit = /[0-9]/;

or the RegExp constructor, useful when the pattern needs to be built dynamically from a string at runtime:

const hasDigit2 = new RegExp('[0-9]');

Testing for matches​

/cat/.test('concatenate');   // true β€” "cat" appears inside the word
/cat/.test('dog'); // false

Sets of characters​

Square brackets define a character class β€” match any one character from the set:

/[aeiou]/.test('sky');    // false β€” no vowels
/[^aeiou]/.test('sky'); // true β€” ^ inside [] negates the set: "any character NOT in this list"

Common classes have shorthand:

  • \d β€” any digit, same as [0-9]
  • \w β€” any "word" character: letters, digits, underscore
  • \s β€” any whitespace character
  • \D, \W, \S β€” the negated versions of each
  • . β€” any character except a newline
/\d{3}-\d{4}/.test('555-0132'); // true

Repeating parts of a pattern: quantifiers​

  • * β€” zero or more
  • + β€” one or more
  • ? β€” zero or one (optional)
  • {n} β€” exactly n
  • {n,m} β€” between n and m
/\d{3}-\d{3,4}/.test('555-013');  // true
/colou?r/.test('color'); // true β€” the "u" is optional
/colou?r/.test('colour'); // also true

Grouping subexpressions and matches​

Parentheses group part of a pattern (so a quantifier can apply to more than one character) and also capture whatever matched that group for later use:

const match = '2026-09-08'.match(/(\d{4})-(\d{2})-(\d{2})/);
match[0]; // "2026-09-08" β€” the full match
match[1]; // "2026" β€” first group
match[2]; // "09"
match[3]; // "08"

Named groups make the intent much clearer, especially with several captures:

const dateMatch = '2026-09-08'.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/);
dateMatch.groups.year; // "2026"
dateMatch.groups.month; // "09"

Boundaries and look-ahead​

\b matches a word boundary β€” the (zero-width) transition between a word character and a non-word character β€” useful for matching whole words instead of substrings:

/\bcat\b/.test('concatenate'); // false β€” "cat" here isn't a whole word
/\bcat\b/.test('the cat sat'); // true

A look-ahead (?=...) asserts that something follows, without consuming it as part of the match β€” handy for password-strength-style checks:

const hasDigitAhead = /^(?=.*\d).{6,}$/; // at least 6 chars, containing at least one digit somewhere
hasDigitAhead.test('abcdef'); // false β€” no digit
hasDigitAhead.test('abc123'); // true

Choice patterns​

| means "or," and combines naturally with grouping:

/cat|dog|bird/.test('I have a dog'); // true
/gr(a|e)y/.test('grey'); // true β€” matches "gray" or "grey"

The mechanics of matching, and backtracking​

A regex engine tries to match starting at each position in the string, left to right, and within a match it tries the longest possible option first for quantifiers like * and + (this is what "greedy" means) β€” backtracking to a shorter option only if the longer one ultimately fails to let the rest of the pattern match.

'<a><b>'.match(/<.*>/)[0];  // "<a><b>" β€” greedy: grabs as much as possible
'<a><b>'.match(/<.*?>/)[0]; // "<a>" β€” lazy (the ?): grabs as little as possible

Greediness is usually what you want (matching the whole quoted string, not just up to the first internal character), but it's a classic source of subtle bugs when parsing things like HTML tags or quoted strings that can appear multiple times on one line β€” reach for the lazy *?/+? variants when you specifically want "the shortest thing that matches," as in the tag example above.

replace, search, and dynamically building patterns​

String.prototype.replace accepts a regex as its first argument, and β€” very powerfully β€” a function as its second, letting you compute each replacement based on what actually matched:

'2026-09-08'.replace(/(\d{4})-(\d{2})-(\d{2})/, '$2/$3/$1');
// "09/08/2026" β€” $1, $2, $3 refer to the captured groups

'price: 42'.replace(/\d+/, (match) => String(Number(match) * 2));
// "price: 84"

Add the g flag to replace (or match) every occurrence instead of just the first:

'a1 b2 c3'.replace(/\d/g, '#'); // "a# b# c#"

search returns the index of the first match (or -1), similar to String.indexOf but pattern-aware; when a regex has the g flag and you call .exec() repeatedly on the same regex object, it remembers where it left off via its lastIndex property β€” useful for walking through every match one at a time in a loop.

Worked example: parsing a small config file​

Config files in the classic "INI" style look like this:

; comment
name = JavaScript Deep Dive
[server]
port = 8080
host = localhost

Lines are one of: blank, a comment (starting with ;), a [section] header, or a key = value pair. A small parser built from a few targeted regexes handles all four cases cleanly:

function parseIni(text) {
const result = { global: {} };
let section = result.global;

for (const rawLine of text.split('\n')) {
const line = rawLine.trim();
if (line === '' || line.startsWith(';')) continue; // blank or comment

const sectionMatch = line.match(/^\[(.+)\]$/);
if (sectionMatch) {
section = result[sectionMatch[1]] = {};
continue;
}

const pairMatch = line.match(/^(\w+)\s*=\s*(.*)$/);
if (pairMatch) {
section[pairMatch[1]] = pairMatch[2];
} else {
throw new SyntaxError(`Unrecognized line: ${rawLine}`);
}
}
return result;
}

This is a genuinely realistic use of regular expressions: each line is checked against a small, focused pattern rather than one giant unreadable regex trying to parse the whole file at once β€” a good rule of thumb any time a pattern starts feeling unmanageable.

Code units and characters​

One subtlety worth knowing: Javascript strings are sequences of 16-bit code units (UTF-16), and some characters β€” many emoji among them β€” need two code units to represent one visible character. . and \w in a regex normally operate per code unit unless you add the u (Unicode) flag, which makes the engine treat such pairs as a single character:

'πŸ™‚'.length;              // 2 β€” two UTF-16 code units, not what you'd expect
[...'πŸ™‚'].length; // 1 β€” spreading a string iterates by actual character
/^.$/.test('πŸ™‚'); // false β€” "." only matches one code unit
/^.$/u.test('πŸ™‚'); // true β€” the "u" flag fixes this

Key takeaways​

  • Character classes and quantifiers describe shapes of text; groups let you both structure a pattern and extract pieces of what matched.
  • Quantifiers are greedy by default (*, +) β€” add ? for a lazy version when you want the shortest match instead of the longest.
  • replace with a function argument is a powerful way to compute context-aware replacements.
  • Prefer several small, targeted regexes over one giant pattern when parsing structured text line-by-line.
  • Add the u flag when working with text that might contain multi-code-unit characters like emoji.

Try it yourself​

  1. Write a regex that matches a simple email address shape: some word characters, an @, more word characters, a ., and 2–4 letters.
  2. Use .replace() with a regex and a function to convert every snake_case_word in a string to camelCase.
  3. Extend parseIni above so it also throws a helpful error if a [section] header appears with no closing bracket.
Hints
  1. Something like /^\w+@\w+\.[a-zA-Z]{2,4}$/ β€” not RFC-compliant, but good enough for form validation.
  2. Match /_([a-z])/g and use the replacer function to uppercase the captured letter.
  3. Check for a line starting with [ that doesn't match the full ^\[(.+)\]$ pattern, and throw there.

Summary​

Regular expressions are a small, dense language of their own, and they earn that density back the first time you use one to replace fifty lines of manual character-checking with a single line. They're most valuable for validation and light parsing β€” for anything structurally deep (a real programming language, deeply nested data), you want an actual parser, which is exactly what we build next.

Β 
Up nextCh. 10 β€” Modules~30 min

🌟 Join the Javascript360 community

Connect with fellow JS developers, and get free interview prep, React challenges, and DSA guides.

Join on WhatsApp β†’