
Случайно увидел результат работы функции RegExp.escape() и был удивлен, потому что она заэкранировала пробелы, все спецсимволы, а также цифры и латинские буквы в начале строки. До появления RegExp.escape() (а она стала доступна в популярных браузерах лишь в 2025 году) я, как и многие другие, писал аналогичную функцию сам, но без экранировки вышеперечисленных символов. Получается, что я ошибался, и нужно бросать все дела, рыться в старых исходниках и переписывать функцию? И да, и нет.
Прежде всего заглянем в стандарт языка JavaScript. Он предписывает экранировать первый символ строки, если тот является цифрой или латинской буквой. И есть объяснение: это нужно для корректного сцепления двух регулярных выражений.
Проще всего объяснить на примерах. В них сцепляются две регулярки, одна из которых создаётся во время работы программы, например вводится пользователем, а значит должна быть экранирована. Вы можете выполнить примеры прямо сейчас в консоли браузера.
Начнём с примера, который показывает необходимость экранировать цифру.
p = '2' // Например, результат вызова функции prompt() new RegExp('()\\1' + RegExp.escape(p)).source // С экранировкой: ()\\1\\x32 new RegExp('()\\1' + p).source // Без экранировки: ()\\12
С экранировкой регулярка будет искать содержимое 1-й группы захвата, за которым следует символ 2, как и задумывал программист. Без экранировки будет искать содержимое 12-й группы захвата. Если групп меньше 12-ти, как в примере выше, то будет искать символ с восьмеричным кодом 12 (я ненавижу восьмеричные числа, а вы?). На мой взгляд, шанс столкнуться с подобной ошибкой крайне невелик.
Переходим к экранировке латинской буквы.
p = 'a' // Например, результат вызова функции prompt() new RegExp('\\c' + RegExp.escape(p)).source // С экранировкой: \\c\\x61 new RegExp('\\c' + p).source // Без экранировки: \\ca
С экранировкой регулярка будет искать трёхсимвольную строку \ca, без экранировки — символ с кодом 1. По-моему эта причина экранировки латинской буквы высосана из пальца, потому что нужна только когда в левой части регулярки скорее всего присутствует ошибка: за \c не следует латинская буква. Вместо экранировки буквы, лучшим решением будет либо исправление ошибки, либо замена \c на \\c. О последовательности \c лучше вообще забыть — это тяжёлое наследие царского режима, подобно восьмеричным числам.
Обратите внимание: в примерах выше не используется Unicode-режим (не указан флаг u или v). Если включить Unicode-режим, то при попытке создать регулярку будет брошено исключение SyntaxError, и программист сразу узнает об ошибке. Это одна из причин, по которой я рекомендую по возможности использовать Unicode-режим. В ESLint есть соответствующее правило.
Хорошо, с цифрами и буквами разобрались. Но почему RegExp.escape() экранирует пробелы и некоторые безобидные спецсимволы, которые не управляют разбором регулярки? В частности, символ @, который русские называют собакой, а казахи — ухом луны. В стандарте языка JavaScript о причинах ничего не сказано.
Давайте сделаем странное и заглянем в исходники популярных браузеров, там иногда можно встретить полезные комментарии. В Firefox тишина, в Chromium тоже. В официальных тестах языка аналогичная ситуация.
Есть еще одно стандартное место для поиска. Это репозитории организации Ecma TC39, которая занимается развитием и стандартизацией JavaScript. Для каждой новой плюшки языка есть свой репозиторий с подробной информацией, RegExp.escape() не исключение. Ага, вот и объяснение:
Per https://gist.github.com/bakkot/5a22c8c13ce269f6da46c7f7e56d3c3f, we now escape anything that could possible cause a “context escape”. This would be a commitment to only entering/exiting new contexts using whitespace or ASCII punctuators. That seems like it will not be a significant impediment to language evolution.
Пробелы и часть символов пунктуации (куда входит ухо луны) экранируются «на всякий случай», «на будущее». Вот такое банальное и, на мой взгляд, спорное решение. Могли бы добавлять экранировку новых символов не всем скопом, а по мере появления их в синтаксисе RegExp.
Кстати, о будущем. Существует предложение добавить новый флаг x, который в регулярке поменяет поведение пробелов: они будут игнорироваться. Но это будущее скорее всего никогда не наступит, потому что предложение за пять лет так и осталось на первой стадии принятия из четырех.
Теперь, после «разоблачения» RegExp.escape(), переходим к вопросу, нужно ли переписывать аналогичную самопальную функцию (если, конечно, она у вас есть). Я с ходу не смог найти подобную функцию в своих исходниках, но скорее всего она была скопирована с незаменимого сайта MDN.
На сайте кода функции уже нет, он заменён на рекомендацию использовать RegExp.escape(). Не беда, исходники сайта хранятся на GitHub, а значит лёгким движением руки мы можем переместиться во времени на 6 лет назад. Помните 2020-й год? Войны нет, ИИ нет, смузи льётся рекой. В общем, всё зашибись... если бы не пандемия. Итак, вот эта функция:
function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
Если не ошибаюсь, здесь экранируются все управляющие символы, используемые текущей версией RegExp. О последовательности \c, как я сказал выше, лучше забыть. Таким образом, функция не покрывает лишь первый рассмотренный выше пример сцепления регулярок. Для очистки совести добавляем экранировку цифры в на��але строки:
function escapeRegExp(string) { const coolCmd = string => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const charCode = string.charCodeAt(0); return charCode >= 0x30 && charCode <= 0x39 ? `\\x${charCode.toString(16)}${coolCmd(string.slice(1))}` : coolCmd(string); }
Теперь старый код будет работать без ошибок.
А как писать новый код? Нужно вместо самопальной функции вызывать RegExp.escape(), при необходимости добавив полифил отсюда или отсюда.
Это кожаная статья, то есть написанная от начала и до конца кожаным автором без помощи нейрогаллюциногенераторов. Если вы отвыкли от подобных статей, она могла показаться вам немного странной.
