Regular Expressions: Stop Copy-Pasting Patterns You Can't Read · 第 5 / 6 节

Lesson 05: Grouping and choices

Lesson objectives:

  • Wrap part of a pattern in a capturing group (…) and read what it captured.
  • Find captured pieces in the match array (index 0 is the whole match; groups follow).
  • Offer alternatives with |, and use a group to control what | applies to.

Prerequisites: Lessons 01-04 (classes, quantifiers, anchors) | Previous << 04 | Next 06 >>

You matched the date — now get the year out of it

You can now write /^\d{4}-\d{2}-\d{2}$/ to validate a date. But validating isn't extracting: knowing a string is a date doesn't hand you the year. And what about a pattern that should accept either "gray" or "grey", or either "cat" or "dog"? Both needs point at the same two metacharacters. Parentheses do double duty — they bundle part of a pattern together and remember what that part matched — while | offers a choice. The catch is that | reaches further than most people expect, and parentheses are how you rein it in.

Explanation

A capturing group is part of a pattern wrapped in parentheses. It does two jobs at once. First, it bundles tokens so a quantifier can apply to the whole group: in /(ab)+/, the + repeats "ab", matching "ab", "abab", "ababab". Second, it remembers what it matched so you can pull it out afterward: /(foo)/ matches and remembers "foo"1.

Where does the remembered text go? Into the array that match returns. Without the global flag, match returns the first match as an array: element 0 is the whole match, and each capturing group's text follows in order — index 1 is the first group, index 2 the second, and so on2. Matches to capturing groups appear in the same order as the left parentheses1.

That is how you extract: put parentheses around the pieces you want, then read them at index 1, 2, and so on.

Sometimes you want the grouping without the capturing — for example, to apply a quantifier to a bundle you don't care to extract. A non-capturing group (?:…) does exactly that: it groups the tokens but does not add an entry to the result array1. Reach for it when the parentheses are only there to hold a quantifier or an alternation together.

Now the choice operator. The disjunction | separates alternatives; the engine tries the left alternative first, and if it fails, the right one3. So /cat|dog/ matches "cat" or "dog". The subtlety — and it causes real bugs — is precedence: | has the lowest precedence of any regex operator3. That means it splits the pattern at the widest possible point. /^cat|dog$/ is not "the whole string is cat or dog"; it parses as (^cat) or (dog$) — starts with "cat", or ends with "dog".

To make | apply to just a slice of the pattern, group that slice3. /^(cat|dog)$/ puts the alternation inside parentheses, so now the anchors wrap the choice: the whole string must be exactly "cat" or exactly "dog". Same idea with gr(a|e)y, which matches "gray" or "grey" while keeping the shared "gr" and "y" outside the choice.

Worked example (follow along)

You want to pull the hours and minutes out of a time string, not just validate it. Wrap each piece in a group and read them back:

Step by step: the pattern is (\d{2}):(\d{2}). The first (\d{2}) captures two digits into group 1, the literal : matches the colon (and is not captured, since it's outside any parentheses), and the second (\d{2}) captures two digits into group 2. After match, element 0 is the full "09:30", element 1 is "09", element 2 is "30". The parentheses turned a validator into an extractor.

Your turn (faded example)

You want a pattern that matches either "cat" or "dog" as the entire string — nothing before or after. Fill in the group so the anchors wrap the choice:

Answer: the blank is (cat|dog), giving /^(cat|dog)$/. The parentheses limit the | to just "cat" versus "dog", and the anchors ^ and $ then apply to the whole group, so the entire string must be exactly one of the two words. Without the parentheses — /^cat|dog$/ — the low precedence of | would let "category" through, because it starts with "cat".

Summary + what's next

Parentheses make a capturing group: they bundle tokens for a quantifier and remember the matched text, which shows up in the match array at index 1 and beyond (index 0 is always the whole match). Use (?:…) when you want grouping without capturing. The | operator offers a choice but has the lowest precedence, so it splits at the widest point — wrap it in a group to control its reach. One thing still isn't obvious: when a quantifier like + can match short or long, how much does it take? Next we settle greedy versus lazy — and then you build and verify a real validator end to end.

Footnotes

  1. MDN: Groups and backreferences — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions/Groups_and_backreferences 2 3

  2. MDN: String.prototype.match() — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match

  3. MDN: Disjunction (the | operator) — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Disjunction 2 3

Exercises

Exercise 1 · Level 1 (warm-up)

In your console, run "file: report.pdf".match(/(\w+)\.(\w+)/). Read elements 0, 1, and 2 of the result and say what each one is.

Done criteria · checked locally
Exercise 2 · Level 2 (advanced)

Write a pattern that matches a whole string that is either "yes" or "no" (case-sensitive), then extract which one was given. Test against "yes", "no", and "maybe". Then explain why /^yes|no$/ (no parentheses) is wrong.

Done criteria · checked locally