При работе со строками очень часто нужно находить что-то в строке в разных местах и перемешивать это в каком-то другом порядке. Порядок элементов не всегда может быть последовательный и отделённый друг от друга. Часть строки может по смыслу попадать в две и более секции. Из-за обилия круглых скобок в шаблоне можно немного потеряться.
Далеко ходить не нужно. Например если нам нужно менять строку формирования даты:
let date1 = '30-09-2023';
Сейчас данная строка состоит из 3 смысловых частей, отделённых друг от друга символом дефиса. Первым по смыслу идёт день месяца, вторым номер месяца в году и третьим сам год.
Представьте, что нам нужно поменять логику оформления дат такого вида. Например мы сначала хотим получать год, потом месяц, потом день. Как это сделать?
Использование захватов в шаблонах регулярных выражений для переворачивания строковой даты в JavaScript
Мы начнём с простого случая, когда захваты идут отдельно друг от друга и не вкладываются или не объединяются. Для переворота строкового вида даты, в этом случае, мы воспользуемся методом replace() в сочетании с синтаксисом захвата. Мы можем использовать числовую нумерацию захватов, которая высчитывается автоматически.
let date1 = '30-09-2023'; let date2 = date1.replace(/^(..)-(..)-(....)$/g, '$3-$2-$1');
На выходе в date2 мы получим ‘2023-09-30‘

Также мы можем воспользоваться именованием захватов, для более понятной смысловой перестановки. Для этого внутри захватов мы ставим в самом начале знак вопроса «?«, который является «спецификатором группы», а затем внутри угловых скобок записываем «имя группы захвата». Всё кроме имени является синтаксисом шаблона регулярного выражения.
let date3 = '30-09-2023'; let date4 = date3.replace(/^(?<day>..)-(?<month>..)-(?<year>....)$/g, '$<year>-$<month>-$<day>'); date4 //'2023-09-30'
На выходе в date4 мы получим ‘2023-09-30‘

Это был простой пример. Каждый захват открывался и закрывался отдельно от другого. То есть, символы круглых скобок чередовались. Сначала появлялась левая круглая скобка, а затем мы видели правую круглую скобку. И так ещё два раза внутри шаблона.
Как увидеть состав захватов в шаблонах регулярных выражений JavaScript?
Чтобы понимать какие части строки захватывает шаблон, мы будем использовать метод exec() у наших экземпляров регулярных выражений.
Круглые скобки в шаблонах регулярных выражений JavaScript обозначают захваты
Нужно знать, что захваты могут вкладываться друг в друга и образовывать отдельные захваты.
Шаблон № 1 — два уровня вложенности захватов
Теперь давайте придумаем совершенно случайный пример и потренируемся на нём.
let str = 'qweasdzxc';
Для простоты восприятия шаблон будет симметричным.
/^(.)((.)(.))(.)/g.exec(str);
Мы получим экземпляр массива с шестью элементами
["qwea","q","we","w","e","a"];
Сколько пар круглых скобок мы создали в шаблоне? 5 пар.

Почему массив состоит из 6 элементов?
Как работает метод exec() для регулярных выражений в JavaScript?
Всё дело в том, что под нулевым индексом массива всегда будет храниться полный результат найденного сопоставления шаблона со строкой. Чтобы это понять, нужно обратиться в официальную документацию ECMAScript для метода exec().

Метод exec() возвращает результат вызова абстрактной операции RegExpBuiltinExec().
Внутри этой операции создаётся новый массив с длиной на 1 больше, чем количество созданных захватов (обнаруженных захватов).

Для индекса «0» в этом новом массиве уже заготовлено местечко для результата выполнения другой абстрактной операции GetMatchString(). Её задача вернуть полный результат сопоставления шаблона для строки. Задача вернуть то, что удалось подобрать под весь шаблон — последовательность символов.

Именно такой сдвиг на один элемент массива позволяет логично понимать шаблон захватов и давать соответствующие номера. У нас пять захватов, поэтому индексы для наших захватов начинаются с «1» и заканчиваются «5«.
Теперь мы можем вернуться к нумерации. В каждом захвате на самом глубоком уровне мы пытались сопоставить один любой символ строки (оформляется точкой). Таких односимвольных захватов у нас четыре штуки. Второй и третий из них обёрнуты ещё одним захватом — пятым. В результате мы имеем захват, который состоит только из захватов, без своего уникального способа сопоставления — он не пытается отыскать что-то ещё.
(.)((.)(.))(.) q, we, w, e, a
Сперва мы получаем один символ «q«, а потом мы получаем два символа «we«.
При первом изучении регулярных выражений у новичка может сложиться некоторый конфликт. Новичок может посчитать, что первыми должны обрабатываться захваты одного уровня вложенности и только потом сопоставление должно переходить на новый уровень. Звучит логично, но в случае с регулярными выражениями логика нумерации захватов привязана к «символу левой круглой скобки» в последовательности слева-направо для шаблона регулярного выражения.
В документации ECMAScript можно найти подтверждение этой логике. Существуют две абстрактные операции которые оценивают захваты в шаблоне регулярного выражения:
- CountLeftCapturingParensWithin ( node ) — принимает узел аргумента (узел синтаксического анализа) и возвращает неотрицательное целое число. Он возвращает количество левых скобок в узле. Левая скобка — это любой символ ( шаблона, который соответствует ( терминалу Atom :: ( GroupSpecifieropt Disjunction ) продукции.
- CountLeftCapturingParensBefore ( node ) — принимает узел аргумента (узел синтаксического анализа) и возвращает неотрицательное целое число. Он возвращает количество левых скобок в охватывающем шаблоне, которые встречаются слева от узла.
Исходя из этого знания мы понимаем, что глубина захватов действительно учитывается, но только для корректной оценки сопоставления строки. По факту, в итоговом массиве нумерация захватов тупо соответствует позиции левой круглой скобки во всём шаблоне регулярного выражения.
Отсюда получается, что в нашем примере второй круглой скобкой является захват, состоящий из двух захватов. Поэтому второй элемент массива это строка «we«, а не «w«.
Третья левая круглая скобка — это захват второго символа строки — «w«. Напомню, что наш шаблон сопоставления начинался с символа домика «^«, который является утверждением.
Ну а дальше логика уже понятна и других вложенностей захватов нет.
Шаблон № 2 — три уровня вложенности захватов
Предлагаю на том же примере строки добавить ещё один уровень вложенности для захватов
let str = 'qweasdzxc'; /^(.)((.)((.)(.)))(.)/g.exec(str);
Итог содержимого захватов будет таким:

Информационные ссылки
Стандарт ECMAScript — https://tc39.es/ecma262/multipage/