Привет, Хабр!
Сегодня рассмотрим флаг регулярных выражений v в JavaScript. Флаг поддержан в современных движках и Node 20+, а для старых окружений есть транспиляция через Babel. Начнём с краткой ориентации где это уже работает и почему синтаксис отличается, а потом пойдём в практику.
Что такое v и почему это не просто u++
Флаг v включает режим unicodeSets. Это отдельный вариант интерпретации шаблона: u и v нельзя смешивать одновременно в одном регексе. В v режиме доступны:
-
свойства строк Юникода через \p{…}, т.е совпадения могут быть не только одиночными кодовыми точками, но и последовательностями;
-
расширенная запись символьных классов с вложенностью и операциями пересечения и вычитания;
-
исправленная логика для комплементарных классов с флагом i.
Поддержка по браузерам и Node стабильна: Chrome с 112, Firefox с 116, Safari с 17, Node начиная с 20. Для лего-совместимости в сборке есть плагин @babel/plugin-transform-unicode-sets-regex, он уже входит в preset-env и переписывает v в эквивалент под u, насколько это возможно.
Коротко про синтаксис: классы, пересечения, вычитания
В v режиме можно писать внутри одного символьного класса выражения-множества:
-
Пересечение:
&& -
Вычитание:
-- -
Юнион: просто перечисление без оператора
-
Вложенность: разрешена, чтобы группировать операнды
Нельзя на одном уровне смешивать && и -- — группируйте вложенными [...]. И помните: некоторые символы внутри v-классов нельзя ставить как есть из-за конфликта с двойными пунктуаторами, иначе будет SyntaxError.
Комплементарный класс [^…] в v — это комплемент множества, а не отрицание результата, благодаря чему поведение с флагом i становится ожидаемым и согласованным с \P{…}.
Далее смотрим что там с кодом.
Пересечение: фильтруем только греческие буквы, а не знаки
// Пересечение Script_Extensions=Greek с Letter const reGreekLetters = /[\p{Script_Extensions=Greek}&&\p{Letter}]/v; reGreekLetters.test('π'); // true reGreekLetters.test(' '); // false (это OGHAM SPACE MARK) reGreekLetters.test('ᾀ'); // true (греческая буква с диакритикой)
Почему именно Script_Extensions, а не Script: первый включает символы, которые принадлежат нескольким скриптам, и его чаще ожидают в валидациях. С пересечением выражаем это без lookahead и без огромных перечислений диапазонов.
Вычитание: все десятичные цифры, кроме ASCII
// Совпадает с любой «десятичной цифрой» Юникода, кроме ASCII 0-9 const reNonAsciiDigit = /[\p{Decimal_Number}--[0-9]]/v; reNonAsciiDigit.test('٤'); // true (арабско-индийская цифра 4) reNonAsciiDigit.test('4'); // false
Зачем это: при нормализации пользовательского ввода можно быстро найти все не-ASCII цифры и либо отклонить, либо преобразовать. Это адекватнее, чем пытаться вручную перечислять блоки.
Теперь сделаем функцию нормализации строки с заменой любых десятичных цифр Юникода на ASCII. В JS нет готового API, которое вернёт цифровое значение символа из Юникода, поэтому используем известные диапазоны десятичных цифр.
// Преобразует все десятичные цифры Юникода к ASCII 0-9 export function normalizeDecimalDigits(input) { // Быстрая проверка: есть ли вообще не-ASCII цифры if (!/[\p{Decimal_Number}--[0-9]]/v.test(input)) return input; // Поддержанные диапазоны «нулей» для Decimal_Number // (добавляйте при необходимости — шаблон легко расширяется) const zeros = [ 0x0660, // Arabic-Indic 0x06F0, // Extended Arabic-Indic 0x07C0, // N'Ko 0x0966, // Devanagari 0x09E6, // Bengali 0x0A66, // Gurmukhi 0x0AE6, // Gujarati 0x0B66, // Oriya 0x0BE6, // Tamil 0x0C66, // Telugu 0x0CE6, // Kannada 0x0D66, // Malayalam 0x0E50, // Thai 0x0ED0, // Lao 0x0F20, // Tibetan 0x1040, // Myanmar 0x17E0, // Khmer 0x1810, // Mongolian 0xFF10 // Fullwidth ]; const mapDigit = (cp) => { for (const z of zeros) { const delta = cp - z; if (delta >= 0 && delta <= 9) return String.fromCharCode(0x30 + delta); } return null; // не цифра из поддержанных диапазонов }; let out = ""; for (let i = 0; i < input.length; ) { const cp = input.codePointAt(i); const repl = mapDigit(cp); out += repl ?? String.fromCodePoint(cp); i += cp > 0xFFFF ? 2 : 1; } return out; } // Пример // "٠١٢٣٤5٦789" => "0123456789"
Регексп не делает замену сам, у него задача выделить класс. Диапазоны взяты из стандартных блоков цифр Юникода — список легко проверить в спецификациях Юникода и адаптировать под свои регионы.
Свойства строк: наконец-то совпадения длиннее одной точки кода
С \p{…} в режиме u вы уже могли обращаться к свойствам символов. В режиме v те же \p{…} могут ссылаться на свойства строк. Сейчас это в первую очередь RGI-эмодзи: корректные последовательности с модификаторами, вариационными селекторами, ZWJ и флагами. Шаблон ^\p{RGI_Emoji}$ в v режиме совпадает и с одиночным эмодзи, и с составными последовательностями.
// Ровно один RGI-эмодзи (символ или валидная последовательность) const reEmoji = /^\p{RGI_Emoji}$/v; reEmoji.test('⚽'); // true reEmoji.test('👨🏾⚕️'); // true reEmoji.test('😎'); // true reEmoji.test('A'); // false
Плюс доступен литерал строк внутри класса: \q{…}. Это даёт возможность делать операции множеств и со строками, не только с одиночными символами:
// Исключим строго один конкретный эмодзи-паттерн из множества RGI // \q{...} — литерал строки в классе. Можно перечислять через | const reEmojiExceptEngland = /^[\p{RGI_Emoji_Tag_Sequence}--\q{}]$/v; reEmojiExceptEngland.test(''); // true — любой другой теговый флаг reEmojiExceptEngland.test(''); // false — именно England
Список поддержанных свойств строк в спецификации включает RGI_Emoji и его подтипы для кейкапов, флагов и ZWJ-последовательностей. Идея в том, что движок разворачивает свойство в набор альтернатив, упорядоченных от длинных к коротким, чтобы префиксы не съедали более длинные варианты.
Кейсы
1) Валидация логина по правилам: латиница, кириллица, цифры, дефис, без подчёркиваний, длина 3–24
// Разрешаем буквы и цифры любых скриптов ИЛИ дефис. // Для строгой ASCII-версии пересекаем с \p{ASCII}. const ALLOWED = /^(?:[\p{Letter}\p{Number}-]{3,24})$/v; export function isValidLogin(s) { // Дополнительно запретим ведущий/хвостовой дефис и подряд двойные дефисы if (!ALLOWED.test(s)) return false; if (/^-|-$|--/.test(s)) return false; return true; }
Если нужна строгая ASCII-версия, замените класс на [\p{Letter}&&\p{ASCII}\p{Number}&&\p{ASCII}-] с правильной группировкой. Это наглядней, чем ручные диапазоны.
2)Нормализуем пробелы: только ASCII-пробелы, все остальное — в обычный пробел
const reAsciiWhitespace = /[\p{White_Space}&&\p{ASCII}]+/v; const reAnyWhitespace = /\p{White_Space}+/v; export function squeezeSpaces(s) { // Сначала приводим все виды whitespace к пробелу const step1 = s.replace(reAnyWhitespace, ' '); // Затем ужимаем группы ASCII-пробелов и тримим return step1.replace(reAsciiWhitespace, ' ').trim(); }
Подход изолирует ASCII-пробелы от остальных и не трогает нестандартные разделители, если это важно для доменной логики.
3) Анти-подмена цифр: ищем наличие не-ASCII десятичных цифр
// Быстрый чек перед парсингом цены/количества export function hasNonAsciiDecimalDigits(s) { return /[\p{Decimal_Number}--[0-9]]/v.test(s); }
Сценарий встречается в платежных формах и админках: не все пользователи вводят латинские цифры. Выявляем и показываем понятную подсказку, а не неверный формат.
4) Подсчёт эмодзи-токенов в сообщении
// Матчим только RGI-эмодзи, без прочих символов const reEmojiToken = /\p{RGI_Emoji}/gv; export function countEmojis(s) { let c = 0; for (const _ of s.matchAll(reEmojiToken)) c++; return c; }
Свойства строк в v режиме дают гранично точное совпадение RGI-эмодзи, включая флаги и ZWJ-последовательности. В u это приходилось собирать вручную через альтернативы.
Нюансы
Нельзя смешивать операторы на одном уровне. Пишите так: [\p{L}&&[\p{Greek}--[α-ω]] ], а не [\p{L}&&\p{Greek}--[α-ω]]. Иначе SyntaxError.
Экранируйте «двойные пунктуаторы» в классах. В v режиме некоторые символы внутри классов не могут стоять буквально — в частности, последовательности, похожие на -- и &&. Ошибка диагностируется как invalid character in class. Экранируйте или разбивайте класс.
\P{…} и свойства строк. В v \p{…} может описывать свойство строки, а \P{…} — только комплемент к свойству символов. Для отрицания свойства строки применяйте вычитание или комплементарный класс
Флаг i и комплемент. В v [^\p{X}], \P{X} и [\P{X}] эквивалентны, поведение стабильно и совпадает по смыслу. В u так не было.
HTML pattern и неожиданная синтаксическая ошибка. Если у вас внезапно сломался клиентский паттерн в форме — проверьте, не компилирует ли браузер его в v-режиме.
Рекомендации по стилю написания регексов с v
Для сложных правил всегда выражайте смысл через множества. Если нужна «латиница без подчёркивания», пишите [\p{Letter}&&\p{ASCII}--[_]].
Для работы с эмодзи используйте только \p{RGI_Emoji} и подсемейства.
Валидации форм в HTML проверяйте в реальных браузерах. Blink компилирует pattern как v, что отличается от старой логики u.
При транспиляции следите за размером паттернов после развёртки свойств строк. Babel-плагин сделает всё корректно, но итоговый класс может стать крупным.
Поддержка уже есть в актуальных браузерах и Node, транспиляция доступна из коробки. Если вы давно хотели навести порядок в своих регекспах, v — удобный момент заняться этим.
Если вам близка идея «управлять кодом, а не костылями», то, вероятно, есть и другой пробел — базовые интерфейсы. Удивительно, но даже опытные разработчики нередко тратят часы на формы, карточки и интерактивность, решая задачи «в лоб». Приглашаем на серию бесплатных уроков, где разбирем это без магии фреймворков, только на чистом HTML, CSS и JavaScript:
-
2 сентября, 20:00 — Первый сайт за 60 минут: без ChatGPT и конструкторов
-
9 сентября, 20:00 — Создадим красивую карточку товара — дизайн и верстка на чистом HTML и CSS
-
22 сентября, 20:00 — Логика интерфейса: как JavaScript оживляет формы
Актуальный стек технологий для решения задач фронтенда на junior+ уровне можно изучить под руководством экспертов на курсе «JavaScript Developer. Basic».
ссылка на оригинал статьи https://habr.com/ru/articles/941054/
Добавить комментарий