1import{t as e}from"./chunk-Cr4thomO2.js";var a={id:`regex-modern`,moduleId:`stdlib`,trackId:`modern-es`,title:`Modern regex`,blurb:`Named groups, lookbehind, the v flag and unicode set notation.`,minutes:11,level:`working`,tldr:{model:"Modern regex removes three old limits: groups can be named, assertions can look behind, and under the `v` flag a character class becomes a set expression with difference and intersection.",gotcha:"Moving from `u` to `v` is a breaking migration, not a free upgrade â new reserved punctuators must be escaped inside classes, so `[a-z&&]` becomes a SyntaxError."},blocks:[{kind:`prose`,md:"Most people's regex vocabulary is frozen somewhere around 1999 â groups are numbered, you can only look ahead, and a character class is a flat list of characters. Everything since ES2018 has been about removing those three limits.\n\nThe payoff is not cleverness, it is **readability**: a pattern with `(?<year>â¦)` and `[\\p{L}--[aeiou]]` says what it means. Every output in this lesson was run on Node 22."},{kind:`heading`,text:`The flags, and what each one buys`},{kind:`compare`,aLabel:`Flag`,bLabel:`What it changes`,rows:[{label:`g`,a:`global`,b:`Keeps lastIndex; required by matchAll and replaceAll`},{label:`i`,a:`ignoreCase`,b:`Case folding â and it folds differently under v`},{label:`m`,a:`multiline`,b:`Makes ^ and $ match at line breaks, not just string ends`},{label:`s`,a:`dotAll`,b:`Lets . match newlines (ES2018)`},{label:`u`,a:`unicode`,b:`Code-point mode, \\p{...} properties, strict escapes`},{label:`v`,a:`unicodeSets`,b:`Everything u does, plus set algebra and string properties (ES2024)`},{label:`d`,a:`hasIndices`,b:`Adds .indices â start/end offsets per group (ES2022)`},{label:`y`,a:`sticky`,b:`Anchors the match at lastIndex exactly; no scanning forward`}]},{kind:`prose`,md:"`u` and `v` are mutually exclusive â `v` is the successor, and you should reach for it by default in new code. Every flag is readable as a boolean property (`re.unicodeSets`, `re.hasIndices`) or all at once as `re.flags`, which is always returned in a canonical order regardless of how you wrote it."},{kind:`heading`,text:`Named capture groups`},{kind:`prose`,md:"`(?<name>â¦)` gives a group a name. That name then works in three places, which is what makes it worth the extra characters:\n\n- in the result, as `match.groups.name`\n- in a replacement string, as `$<name>`\n- inside the pattern itself, as a backreference `\\k<name>`\n\nNumbered groups keep working alongside them, so `match[1]` still resolves â naming is additive."},{kind:`code`,lang:`js`,caption:`One name, three uses`,code:`const re = /(?<y>\\d{4})-(?<mo>\\d{2})-(?<d>\\d{2})/; 2const m = '2026-08-14'.match(re); 3console.log(m.groups.y, m.groups.mo, m.groups.d); 4console.log('2026-08-14'.replace(re, '$<d>/$<mo>/$<y>')); 5 6// \\k<q> forces the closing quote to match the opening one 7const quoted = /(?<q>['"]).*?\\k<q>/; 8console.log(quoted.test(\`"hi"\`), quoted.test(\`"hi'\`));`,output:`2026 08 14 914/08/2026 10true false`},{kind:`callout`,tone:`gotcha`,title:`groups is null-prototype, and match can be null`,md:"`match.groups` is created with `Object.create(null)`, so it has no `hasOwnProperty`, no `toString`, and `groups.constructor` is `undefined`. Use `Object.hasOwn(m.groups, 'y')` or `'y' in m.groups`.\n\nSeparately, a group that exists in the pattern but did not participate in the match is `undefined`, not missing â so destructuring always succeeds and you have to check the value. And of course `str.match(re)` returns `null` outright when nothing matched, which is the crash people actually ship."},{kind:`heading`,text:`Lookbehind`},{kind:`prose`,md:'`(?<=â¦)` and `(?<!â¦)` are the mirror of lookahead: they assert something about the text to the **left** without consuming it. Before ES2018 the only way to express "a number preceded by a dollar sign" was to capture the `$` and then slice it back off.\n\nUnlike some other engines, JavaScript\'s lookbehind is **variable length** â `(?<=\\$\\s*)` is legal.'},{kind:`code`,lang:`js`,caption:`The context is a filter, not part of the result`,code:`console.log('price $42 and \\u20AC9'.match(/(?<=\\$)\\d+/g)); 11console.log('a1 b2'.match(/(?<!a)\\d/g));`,output:`[ '42' ] 12[ '2' ]`},{kind:`heading`,text:`The d flag: where, not just what`},{kind:`prose`,md:'`d` adds an `indices` array to the match, parallel to the match array itself: entry 0 is the whole match, then one `[start, end)` pair per numbered group, plus `indices.groups` keyed by name. A group that did not participate gets `undefined` rather than a pair.\n\nThis is what you want for syntax highlighting, editor decorations, or any "underline exactly this part" job â without it you get `match.index` for the whole match and nothing else.'},{kind:`code`,lang:`js`,caption:`Offsets per group`,code:`const m = /(?<year>\\d{4})-(\\d\\d)/d.exec('on 2026-08'); 13console.log(m.indices); 14console.log(m.indices.groups);`,output:`[ 15 [ 3, 10 ], 16 [ 3, 7 ], 17 [ 8, 10 ], 18 groups: [Object: null prototype] { year: [ 3, 7 ] } 19] 20[Object: null prototype] { year: [ 3, 7 ] }`},{kind:`heading`,text:`The v flag: character classes become sets`},{kind:`prose`,md:'This is the big one. Under `u`, `[â¦]` is a flat union of characters and ranges. Under `v` it is a **set expression**, with three operators:\n\n- `--` **difference** â in the left set but not the right\n- `&&` **intersection** â in both\n- juxtaposition â **union**, as before\n\nNested classes are allowed, so `[\\p{L}--[aeiou]]` reads exactly as "a letter, but not a vowel". Under `u` that pattern is a `SyntaxError`;
20 the old workaround was `(?![aeiou])\\p{L}`, which is correct but says nothing.'},{kind:`code`,lang:`js`,caption:`Difference and intersection`,code:`console.log('a'.match(/[\\p{L}--[aeiou]]/v)); 21console.log('b'.match(/[\\p{L}--[aeiou]]/v)?.[0]); 22 23console.log('x'.match(/[\\p{Script=Greek}&&\\p{Letter}]/v)); 24console.log('\\u03C0'.match(/[\\p{Script=Greek}&&\\p{Letter}]/v)?.[0]);`,output:`null 25b 26null 27\u03C0`},{kind:`prose`,md:'`v` also introduces two things a character class could never hold before.\n\n**Properties of strings** â `\\p{RGI_Emoji}`, `\\p{Basic_Emoji}`, `\\p{Emoji_Keycap_Sequence}` and friends match sequences of code points, not single ones. That is the only way a class can match a ZWJ-joined family emoji or a flag.\n\n**String literals in a class** â `[\\q{a|b|hello}]` is a class whose members are whole strings, alternated. Combine it with `--` and you get "any emoji except these three", in one expression.'},{kind:`code`,lang:`js`,caption:`A class that matches more than one code point`,code:`// a ZWJ family sequence: 3 code points j
27oined by U+200D 28console.log(/^\\p{RGI_Emoji}$/v.test('\\u{1F468}\\u200D\\u{1F469}\\u200D\\u{1F467}')); 29 30console.log('a|b'.match(/[\\q{a|b}]/v)?.[0]);`,output:`true 31a`},{kind:`callout`,tone:`warn`,title:`v also changes what needs escaping`,md:"Under `v`, the punctuators `( ) [ ] { } / - \\ |` and the double punctuators `&& !! ## $$ %% ** ++ ,, .. :: ;; << == >> ?? @@ ^^ \\`\\` ~~` are reserved inside a class and must be escaped. So a pattern like `[a-z&&]` that was fine under `u` becomes a `SyntaxError` under `v`.\n\nThat makes `u` to `v` a **breaking** migration for hand-written classes, not a free upgrade. Case folding under `vi` also differs from `ui` for complemented classes. Test before you swap the flag across a codebase."},{kind:`heading`,text:`Iterating matches without the state trap`},{kind:`prose`,md:"`String.prototype.matchAll` returns an iterator of full match objects â groups, `index`, `indices` and all â which is what a `while ((m = re.exec(s)))` loop gives you but without the hazards.\n\nIt throws a `TypeError` if the regex lacks `g`, and it works on an **internal clone**, so it never mutates your regex's `lastIndex`. That matters because a module-level `/â¦/g` regex is shared state: two callers using `.test()` or `.exec()` on it will see each other's position."},{kind:`code`,lang:`js`,caption:`Groups survive the iteration`,code:`const pairs = [...'a1b2'.matchAll(/(?<l>[a-z])(?<n>\\d)/g)]; 32console.log(pairs.map((m) => m.groups.l + m.groups.n));`,output:`[ 'a1', 'b2' ]`},{kind:`callout`,tone:`new`,title:`Two more that are landing`,md:"**Duplicate named groups** (ES2025) let the same name appear in mutually exclusive alternatives â `/(?<y>\\d{4})-(?<m>\\d\\d)|(?<m>\\d\\d)\\/(?<y>\\d{4})/` â so one branch or the other fills `groups.y`. Older engines reject it at parse time with *Duplicate capture group name*; Node 22 is one of them, so it is a feature to know about rather than to rely on.\n\n**`RegExp.escape`** (ES2025) finally gives a built-in way to quote user input for interpolation into a pattern. It is also absent on Node 22. Until it is everywhere, `s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')` remains the hand-rolled version."},{kind:`callout`,tone:`interview`,title:`How this gets asked`,md:'Usually as *"what is the v flag for"* â the answer they want is set operations and properties of strings, not "it is the new u". A good follow-up to volunteer: it is a **breaking** change because of the new reserved punctuators.\n\nThe other common one is *"why is this /g regex returning false every other call"*, which is `lastIndex` on a shared regex; the fix is `matchAll`, a fresh literal per call, or resetting `lastIndex` yourself.'},{kind:`takeaways`,items:[`Named groups work three ways: match.groups.x, $<x> in replacements, \\k<x> as a backreference.`,`match.groups is a null-prototype object; non-participating groups are undefined, and match itself can be null.`,`Lookbehind (?<=) and (?<!) assert leftward without consuming, and may be variable length.`,`The d flag adds .indices â a start/end pair per group, plus .indices.groups.`,`The v flag makes classes set expressions: -- difference, && intersection, nested classes.`,`Only v supports properties of strings (\\p{RGI_Emoji}) and string members ([\\q{a|b}]).`,`u to v is breaking: new punctuators must be escaped inside classes.`,`matchAll needs g, clones the regex, and never touches your lastIndex.`]}],cards:e};export{a as REGEX_MODERN};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.