skip to content

Regular Expressions

Regular expressions in JavaScript span pattern syntax, group capture, and the matching and replacing APIs on RegExp and String. Interviewers ask because regexes reveal both precision (do you know what /g does to lastIndex?) and judgement (when a parser beats a pattern).

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

explore

questions

15

In a JavaScript regular expression, what is the difference between (Mr|Ms) and (?:Mr|Ms), and when would you choose the non-capturing form?

level: juniorimportance: must knowfreq 62%

answer

  1. parentheses do two jobs at once
  2. grouping versus recording the text
  3. group numbers come from opening parens
  4. (?: keeps the result array clean

basics

~20 s

Both parenthesis forms group a sub-pattern so quantifiers and alternation apply to the whole unit. Plain parentheses also capture, adding the matched text to the result array; (?: ... ) groups without capturing, so it never occupies a group number.

solid answer

~40 s

Parentheses in a JavaScript regex do two jobs: they group a sub-pattern into one unit, and they capture the text that unit matched. `(?:` turns off only the capturing half. Capturing groups are numbered by the order of their opening parenthesis, left to right, and their matched text shows up as `m[1]`, `m[2]`, ... on the match object returned by `exec`. Use `(?:` whenever you only need grouping — for an alternation like `(?:Mr|Ms)` or to apply a quantifier to several tokens — because every unnecessary capture adds a slot you must skip over and, worse, renumbers every group after it the moment someone edits the pattern. Named groups are the third option when a captured value is genuinely meaningful.

code

javascript · 7 lines
javascript
const withCapture = /(Mr|Ms)\.\s(\w+)/;
const a = withCapture.exec('Mr. Smith');
console.log(a[1], a[2]); // 'Mr' 'Smith'

const noCapture = /(?:Mr|Ms)\.\s(\w+)/;
const b = noCapture.exec('Mr. Smith');
console.log(b[1], b[2]); // 'Smith' undefined

go deeper

for a junior

Know that plain parentheses both group and capture while (?: ... ) only groups, and be able to point at which element of the match result each group fills.

for a middle

Explain that numbering follows opening parentheses left to right, so inserting one capture renumbers every later group, and that a quantified group retains only its final iteration.

for a senior

Make the maintenance argument out loud: positional captures couple consumer code to pattern edits, and you promote a value to a named group as soon as more than one capture is meaningful.

for a principal

Own the convention for patterns that outlive their author — capture only what a caller consumes, prefer named groups in shared patterns, and know when a regex has grown into something that should be a small parser instead.

## Two jobs behind one piece of syntax A pair of parentheses in a JavaScript regular expression does two independent things. It **groups**: the enclosed sub-pattern becomes a single unit, so a quantifier or an alternation bar applies to all of it rather than to the last character. `/ab+/` repeats only `b`; `/(?:ab)+/` repeats the pair. Alternation is the other classic case — `/^(?:cat|dog)$/` anchors both branches, while `/^cat|dog$/` does not. It **captures**: the text the unit matched is recorded and handed back with the match result. That is the half `(?:` switches off. ## What capturing actually produces `RegExp.prototype.exec` returns an array-like match object. Element `0` is the entire matched substring; elements `1`, `2`, `3`, … are the substrings matched by the capturing groups. Group numbers are assigned by the position of the **opening** parenthesis, counted left to right through the whole pattern, nesting included — so in `/((a)(b))/` group 1 is the outer group, group 2 is `a`, group 3 is `b`. ```js const withCapture = /(Mr|Ms)\.\s(\w+)/; const a = withCapture.exec('Mr. Smith'); console.log(a[1], a[2]); // 'Mr' 'Smith' const noCapture = /(?:Mr|Ms)\.\s(\w+)/; const b = noCapture.exec('Mr. Smith'); console.log(b[1], b[2]); // 'Smith' undefined ``` The second pattern still needs the parentheses — without them `Mr|Ms\.\s(\w+)` would mean "`Mr`, or `Ms` followed by a dot" — but it does not need the captured value, so `(?:` keeps the honorific out of the result and slides the surname down to `m[1]`. ## Why numbering fragility is the real argument The cost of a stray capture is not memory, it is coupling. Consumer code reads positions: `m[1]`, `m[2]`. Add one capturing group near the front of a pattern six months later and every position after it shifts by one, silently. Nothing throws; the code just starts reading the wrong field. Patterns that capture only what a caller consumes are far more resistant to that edit. This is also why named groups exist: `(?<surname>\w+)` gives the value a stable identity that survives renumbering entirely. ## A quantified group keeps only the last iteration A subtlety worth having ready: repeating a capturing group does not accumulate the repetitions. The group's stored capture is overwritten on each iteration, so only the final one survives. ```js console.log(/(\w)+/.exec('abc')[1]); // 'c' — last iteration wins console.log(/(\w+)/.exec('abc')[1]); // 'abc' — quantifier inside the group ``` If you want the whole run, put the quantifier **inside** the group. If you want each item separately, you need repeated matching, not a repeated group. ## Nesting and the empty-match case Groups nest freely, and an inner group is numbered after the outer one that contains it. A group that is present in the pattern but never participates in the successful match — for example the untaken branch of an alternation, or an optional group that matched nothing — yields `undefined` in the result array, not an empty string. That distinction matters when you feed captured values straight into string concatenation. ## Performance, honestly stated Non-capturing groups are sometimes sold as an optimisation. In modern engines the difference is negligible for ordinary patterns; capturing means the engine must save and restore a position pair on backtracking, which is real but tiny. Choose `(?:` for clarity and edit-safety and treat any speed benefit as a rounding error. The genuinely expensive regex problems come from backtracking behaviour, not from how many groups you declared. ## Choosing between the three forms A practical rule: use `(?:` by default when you are only grouping; use a plain capturing group when a caller reads the value positionally and the pattern is small; use a named group `(?<name>…)` as soon as the pattern has more than one or two meaningful captures, or when the pattern is likely to be edited by someone who did not write it. The three forms cost the same to type, and the choice is a readability decision that shows up in every line of code that consumes the match.

  • If a pattern contains no capturing groups at all, what does the match result look like?
    You still get element `0`, the full matched text, plus the usual `index` and `input` properties — there are simply no numbered elements after it. The `groups` property is `undefined` because `groups` is only created when the pattern declares at least one named group.
  • How are groups numbered when they nest, and why does that trip people up?
    Numbering follows the order of the opening parenthesis, scanning left to right, so an outer group always gets a lower number than the groups inside it. In `/((a)(b))/` the outer group is 1, `a` is 2, `b` is 3. People expect completion order or innermost-first, and read the wrong slot.
  • What does /(\d)+/ capture in group 1 when it matches "123", and how do you capture the whole run instead?
    Group 1 holds `'3'`. The group is entered three times and its capture is overwritten each iteration, so only the last one survives. Move the quantifier inside the group — `/(\d+)/` — to capture `'123'` as a single unit.

saying these in an interview costs you the question

  • Thinks (?: changes what text the pattern matches
  • Says non-capturing groups exist mainly for speed
  • Believes group numbers follow closing parentheses or nesting depth
  • Expects a repeated group to collect every iteration
  • Assumes an unmatched group yields an empty string

context

open as a page

In JavaScript, what does String.prototype.replace do when you pass a plain string as the pattern, and how do replaceAll and a global regex differ from it?

level: juniorimportance: must knowfreq 72%

basics

~20 s

replace() with a string pattern, or with a regex that lacks the g flag, substitutes only the first occurrence. Replacing every occurrence needs replaceAll() or a /g regex; replaceAll throws a TypeError if handed a non-global regex.

open as a page

In JavaScript, what is the difference between writing a regular expression as the literal /\d+/ and as new RegExp("\\d+"), and when do you actually need the constructor?

level: juniorimportance: must knowfreq 65%

basics

~20 s

Both produce the same RegExp. The literal is parsed as regex source, while the constructor takes an ordinary string, so every backslash must be doubled and a bad pattern fails at runtime. Use the constructor only for patterns assembled from runtime values.

open as a page

Explain lookahead and lookbehind assertions in JavaScript regular expressions — what does "zero-width" mean, and give a case where a lookahead is the only clean solution.

level: middleimportance: must knowfreq 66%

basics

~20 s

Lookaround tests whether a sub-pattern matches at the current position without consuming characters, so the matched text excludes it. (?=x) and (?!x) look forward, (?<=x) and (?<!x) look backward, and each can be positive or negative.

open as a page

A module declares `const RE = /^\d+$/g;` and a validate(value) function that returns RE.test(value). Callers report that the same valid value passes on one call and fails on the next. Why, and how do you fix it?

level: middleimportance: must knowfreq 58%

basics

~20 s

A regex with the g flag carries mutable state: test and exec resume from its lastIndex property and advance it after each match, so a shared regex object alternates between matching and failing. Drop the g flag for a pure test, reset RE.lastIndex = 0, or build a fresh regex per call.

open as a page

In JavaScript, how does String.prototype.match behave with and without the g flag, and what does String.prototype.matchAll give you that match does not?

level: middleimportance: must knowfreq 62%

basics

~20 s

Without the g flag, match returns a rich result for the first match — the matched text, its captures, index and input. With g it returns a flat array of matched strings and drops all of that detail. matchAll returns an iterator of the rich results for every match, and requires a global regex.

open as a page

Given the JavaScript string "<a><b>", what does the regex /<.+>/ match and what does /<.+?>/ match, and what rule explains the difference?

level: middleimportance: must knowfreq 62%

basics

~20 s

/<.+>/ matches the whole "<a><b>" because + is greedy and takes as much as it can. /<.+?>/ matches only "<a>" because the ? after the quantifier makes it lazy, taking the fewest characters that still let the rest of the pattern succeed.

open as a page

Why does /^cat|dog$/.test('hotdog') return true in JavaScript, and how do you write the pattern you probably meant?

level: middleimportance: should knowfreq 42%

basics

~20 s

Alternation has the lowest precedence in a regex, so the pattern splits into two whole branches: ^cat, or dog$. 'hotdog' ends with dog, satisfying the second branch. Group the branches — /^(?:cat|dog)$/ — to make the anchors apply to both.

open as a page

What do named capture groups (?<name>...) give you in a JavaScript regex, how do you read them off a match, and what does \k<name> do?

level: middleimportance: should knowfreq 55%

basics

~20 s

Named groups label a capture so it can be read by name from the match object's groups property instead of by a fragile position number. \k<name> is a backreference: it matches the exact text that named group already captured.

open as a page

In JavaScript's String.prototype.replace, what arguments does a replacer function receive, and when would you use one instead of a replacement string containing $& or $1?

level: middleimportance: should knowfreq 46%

basics

~20 s

A replacer function is called once per match with the whole matched text, then each capture group, then the match offset and the full input string (plus a groups object when the pattern has named captures). Its return value is inserted verbatim, with no dollar-sign expansion.

open as a page

In JavaScript regular expressions, what do the m and s flags each change, and which one makes . match a newline?

level: middleimportance: should knowfreq 52%

basics

~20 s

The s flag, called dotAll, is the one that makes . match line terminators. The m flag leaves . completely alone; it changes ^ and $ so they match at every line boundary instead of only at the whole string's start and end.

open as a page

What does adding the u flag to a JavaScript regular expression change, and why does /^.$/ fail to match the string "\u{1F600}" (a single emoji) without it?

level: middleimportance: should knowfreq 36%

basics

~20 s

Without u, a JavaScript regex matches code unit by code unit, and an emoji is two of them, so . matches only half and the anchored pattern fails. The u flag switches the engine to code points and enables the \u{...} and \p{...} escapes.

open as a page

Two variants of the loop `while ((m = re.exec(s)) !== null) { … }` hang forever: one where `re` is `/\d/` and one where `re` is `/\d*/g`. Explain each hang, and say what you would write instead.

level: seniorimportance: should knowfreq 38%

basics

~20 s

Without the g flag, exec always restarts at index 0 and returns the same first match forever. With g but a pattern that can match the empty string, a zero-length match leaves lastIndex unchanged, so the loop re-matches at the same position. Prefer str.matchAll(re), which handles both.

open as a page

A JavaScript service becomes unresponsive when it validates certain inputs with the regex /^(\w+\s?)*$/. Explain what is happening inside the regex engine, and how you would fix the pattern.

level: seniorimportance: should knowfreq 44%

basics

~20 s

The pattern is ambiguous: a nested quantifier lets the same text be split many ways. On input that ultimately fails to match, JavaScript's backtracking engine tries all of them, which grows exponentially with input length and blocks the thread. Rewrite the pattern so each character has one parse.

open as a page

In JavaScript, what does exec return for an optional capture group that did not participate in the match, and what does a backreference to such a group match?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

A group that did not participate yields undefined in the match result, not an empty string. A backreference to it succeeds by matching the empty string, so the reference silently imposes no constraint at all.

open as a page