Случайно увидел результат работы функции 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(), при необходимости добавив полифил отсюда или отсюда.


Это кожаная статья, то есть написанная от начала и до конца кожаным автором без помощи нейрогаллюциногенераторов. Если вы отвыкли от подобных статей, она могла показаться вам немного странной.

Комментарии (14)


  1. Alexdrbnd
    17.03.2026 18:06

    Вот так живешь, пишешь код, а потом бац - оказывается не правильно писал ))


  1. parakhod_1
    17.03.2026 18:06

    Долго пытался понять что означает слово "экранировать" в данном контексте. Ближе к концу статьи только допёрло, я медленный.

    Ну и да, в регексах действительно и пробелы и прочее подозрительное лучше всегда записывать в безопасном виде. Напишете лишний раз \s, и избавите себя от пары часов ловли необъяснимого бага в самый неподходящий момент.


    1. CoolCmd Автор
      17.03.2026 18:06

      Напишете лишний раз \s

      Только нужно не забывать, что в \s входит куча разных символов, включая разделители строк.


      1. parakhod_1
        17.03.2026 18:06

        В данном примере я условно, конечно.
        Хотя даже тут, на практике, в 99.97% случаев в реальных проектах вы знаете что у вас туда прилетит либо только пробел либо только tab (либо или то или другое), и вас это обычно полностью устраивает.

        Кстати ещё не надо забывать что регексы водятся не только в жабоскрипте, но и во всех современных языках программирования (и не только в языках), и синтакс у них иногда может в мелочах отличаться. И надо всегда стараться писать максимально совместимо, потому что бывает такое что люди копипастят регекс из фронтенда в эликсирный или жабный бэкенд, а потом опять же тратят не один час на ловлю совершенно необязательной проблемы.

        Тут конечно не лениться писать тесты помогает, но тоже не всегда.


  1. Dhwtj
    17.03.2026 18:06

    Архитектурная ошибка: regex - это язык (команды + данные в одной строке, ровно как SQL). Как только тебе нужно вручную подставлять данные в язык - ты получаешь injection-проблему. Escape - это пластырь. Правильный ответ - не смешивать


    1. CoolCmd Автор
      17.03.2026 18:06

      Это не самая большая проблема. Важнее падение читаемости сложных регулярок, таких как проверка адреса электронной почты, которая мне напоминает Brainfuck.


      1. nin-jin
        17.03.2026 18:06

        Поэтому нужно не регулярки развивать, а добавить удобное апи для описания регулярных парсеров, как тут. Побочный эффект такого апи - возможность не только компоновать, парсить и валидировать, но и генерировать строки, гарантированно соответствующие синтаксису.


        1. amcured
          17.03.2026 18:06

          Наверное, имеет смысл сказать, что такой подход (которому лет 30) неплохо гуглится по запросу «parser combinators».


    1. amcured
      17.03.2026 18:06

      А как — не смешивая — разрешить пользователю искать с использованием регулярок? Так-то понятно, что лучше быть здоровым и богатым…


  1. Alexandroppolus
    17.03.2026 18:06

    это нужно для корректного сцепления двух регулярных выражений

    Для этого надо ещё перенумеровать обратные ссылки во втором регексе, экранированием тут не отделаться. Для конкатенация правильно было бы отдельный метод.


    1. CoolCmd Автор
      17.03.2026 18:06

      Можно пример? Экранированная строка (правая) не влияет на разбор итоговой регулярки, в ней нет backreferences.


      1. Alexandroppolus
        17.03.2026 18:06

        При сложении регексов (\d) и (.)\1 должно получиться (\d)(.)\2 , бэкреф теперь на вторые скобки.


        1. CoolCmd Автор
          17.03.2026 18:06

          В статье рассматривается другой случай, когда часть регулярки нужно экранировать и искать просто как текст. В вашем случае экранировка не нужна.

          Чтобы не мучится с номерами групп, можно использовать ссылки на именованные группы.


          1. nin-jin
            17.03.2026 18:06

            Это не спасает, ибо при конфликте имён будет падение. Поэтому при конкатенации регулярок в любом случае нужно переименовывать группы. С позиционными группами тут даже немного проще.