Группы, альтернация и lookahead #
Группы в regex изолируют фрагмент шаблона и сохраняют совпавший текст для последующего использования. Альтернация | выбирает один из вариантов; lookahead и lookbehind проверяют контекст без включения его в результат совпадения. Эти приёмы нужны при разборе статусов заказа, составных адресов и метаданных плейлиста.
Захватывающие группы (...)
#
Скобки группируют часть шаблона и нумеруют захваты слева направо:
const line = 'ord-2026-7f3a | delivered'
const re = /^(ord-\d{4}-[a-f0-9]{4})\s*\|\s*(\w+)$/
const m = line.match(re)
m[0] // полное совпадение
m[1] // "ord-2026-7f3a"
m[2] // "delivered"
В replace группы доступны как $1, $2 или через функцию:
'status: pending'.replace(/status:\s*(\w+)/, 'Статус: $1')
// "Статус: pending"
'1500 RUB'.replace(/(\d+)\s*(\w+)/, (_, sum, cur) => `${sum} ${cur}`)
Вложенные группы увеличивают номера; незахватывающие (?:...) не занимают номер.
Именованные группы (?<name>...)
#
ES2018 добавил именованный захват — читаемее, чем индексы:
const receipt = 'Итого: 2490 RUB, доставка: 300 RUB'
const re = /Итого:\s*(?<total>\d+)\s*(?<currency>\w+)/
const { groups } = receipt.match(re)
groups.total // "2490"
groups.currency // "RUB"
В replace — синтаксис $<name>:
'track: night-drive | 245s'.replace(
/track:\s*(?<slug>[\w-]+)\s*\|\s*(?<dur>\d+)s/,
'$<slug> — $<dur> сек'
)
// "night-drive — 245 сек"
Незахватывающие группы (?:...)
#
Когда группировка нужна только для квантификатора или альтернации, без извлечения:
const tags = 'focus|work|chill'
tags.split(/\|(?:\s*)?/) // ["focus", "work", "chill"]
Сравните с захватывающей группой — в split пустые элементы или лишние части в результате.
Альтернация |
#
Означает «или» — пробует варианты слева направо:
const STATUS = /^(pending|paid|shipped|delivered)$/
STATUS.test('shipped') // true
STATUS.test('cancelled') // false
Приоритет: альтернация имеет низкий приоритет; группируйте скобками:
// совпадение "express" или "standard" после слова delivery
/delivery-(express|standard)/.test('delivery-express') // true
// без скобок — иначе интерпретация иная
/delivery-express|standard/.test('standard') // true — только второй вариант целиком
Практика — способ оплаты на чекауте:
const PAYMENT = /^(card|sbp|cash_on_delivery)$/
function normalizePayment(method) {
const key = method.trim().toLowerCase().replace(/\s+/g, '_')
return PAYMENT.test(key) ? key : null
}
normalizePayment('Cash On Delivery') // "cash_on_delivery"
Lookahead (?=...) и (?!...)
#
Positive lookahead — после текущей позиции должно совпасть ..., но это не входит в результат:
const prices = '1500 RUB 300 USD 90 EUR'
prices.match(/\d+(?=\s*RUB)/g) // ["1500"] — суммы только в рублях
Negative lookahead — далее не должно совпасть:
'товар 1 шт 2 кг'.match(/\d+(?!\s*кг)/g) // ["1"] — число не перед "кг"
Типичный приём — пароль с требованиями без отдельных проходов:
const HAS_DIGIT = /(?=.*\d)/
const HAS_LETTER = /(?=.*[a-zа-я])/i
function passwordOk(pw) {
return pw.length >= 8 && HAS_DIGIT.test(pw) && HAS_LETTER.test(pw)
}
Lookbehind (?<=...) и (?<!...)
#
Positive lookbehind — перед позицией должен быть фрагмент (ES2018):
const log = 'cart:add cart:remove checkout:pay'
log.match(/(?<=cart:)\w+/g) // ["add", "remove"]
Negative lookbehind — перед позицией фрагмента быть не должно:
'цена 1500, скидка 1500'.match(/(?<!скидка\s)\d+/g)
// ["1500", "1500"] — оба числа; уточните шаблон под задачу
Lookbehind требует фиксированной или ограниченной длины слева в части движков; в V8/JavaScript поддерживаются variable-length lookbehind.
Обратные ссылки \1, \k<name>
#
Внутри того же шаблона можно сослаться на ранее захваченную группу:
const dup = 'SAVE-SAVE'
/^(\w+)-\1$/.test(dup) // true — повтор того же слова
const quoted = '"focus"'
/^"(?<w>\w+)"\s*,\s*"\k<w>"$/.test('"focus", "focus"') // false — разные слова
В replace обратные ссылки в шаблоне замены — $1 или $<name>.
Практика: разбор строки доставки #
const DELIVERY_LINE =
/^(?<carrier>express|standard):\s*(?<eta>\d{1,2})\s*мин(?:,\s*трек\s*(?<track>[A-Z0-9-]+))?$/i
function parseDeliveryStatus(line) {
const m = line.trim().match(DELIVERY_LINE)
if (!m) return null
return {
carrier: m.groups.carrier.toLowerCase(),
etaMinutes: Number(m.groups.eta),
track: m.groups.track ?? null,
}
}
parseDeliveryStatus('Express: 45 мин, трек TRK-9X2')
// { carrier: 'express', etaMinutes: 45, track: 'TRK-9X2' }
const PLAYLIST_ENTRY =
/^(?<artist>[^—]+)—\s*(?<title>.+?)\s*\((?<sec>\d{2}):(?<min>\d{2})\)$/
function parseTrack(line) {
const m = line.match(PLAYLIST_ENTRY)
if (!m) return null
const durationSec = Number(m.groups.sec) * 60 + Number(m.groups.min)
return {
artist: m.groups.artist.trim(),
title: m.groups.title.trim(),
durationSec,
}
}
parseTrack('Lumen — Night Drive (04:05)')
// { artist: 'Lumen', title: 'Night Drive', durationSec: 245 }
Итог #
- Захватывающие группы
(...)нумеруются с$1; именованные(?<name>...)читаемее вmatchиreplace. (?:...)группирует без захвата — используйте при альтернации и квантификаторах.- Альтернация
|требует скобок, когда «или» относится только к части шаблона. - Lookahead и lookbehind проверяют контекст без расширения совпадения — удобно для цен в нужной валюте и префиксов в логах.