Методы RegExp и String

Существует два набора методов для работы с регулярными выражениями.

  1. Во-первых, регулярные выражения являются объектами встроенного класса RegExp, он предоставляет много методов.
  2. Кроме того, в обычных строках есть методы, которые могут работать с регулярными выражениями.

Рекомендации

Какой метод использовать, зависит от того, что мы хотели бы сделать.

Методы станет намного легче понять, если мы разобьём их по задачам:

Для поиска всех совпадений:

Используйте флаг g и:

  • Получить плоский массив совпадений – str.match(reg)
  • Получить детализированный набор всех совпадений с возможностью перебора – str.matchAll(reg).

Для поиска только первого совпадения:

  • Получить детализированное первое совпадение – str.match(reg) (без флага g).
  • Получить позицию строки первого совпадения – str.search(reg).
  • Проверить, есть ли совпадение – regexp.test(str).
  • Найти совпадение с заданной позиции – regexp.exec(str) (regexp.lastIndex устанавливает начало поиска в нужную позицию).

Для замены всех совпадений:

  • Заменить на другую строку или результат функции – str.replace(reg, str|func)

Чтобы разбить строку по разделителю:

  • str.split(str|reg)

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

str.search(reg)

Мы уже видели этот метод. Он возвращает позицию первого совпадения или -1, если ничего не найдено:

let str = "Люблю регэкспы я, но странною любовью";

alert( str.search( /лю/i ) ); // 0 (первое совпадение на позиции 0)

Важное ограничение метода search – он всегда ищет только первое совпадение.

Мы не можем найти следующие совпадения с помощью search, для этого просто нет синтаксиса. Но есть и другие методы, которые могут.

str.match(reg), без флага «g»

Поведение str.match изменяется в зависимости от того, имеет ли reg флаг g или нет.

Во-первых, если флаг g отсутствует, то str.match (reg) ищет только первое совпадение.

В результате получается массив с этим соответствием и дополнительными свойствами:

  • index – позиция совпадения внутри строки,
  • input – строка, в которой был произведён поиск.

Например:

let str = "Слава - это жажда молодости";

let result = str.match( /слава/i );

alert( result[0] ); // Слава  (совпадение)
alert( result.index ); // 0 (позиция)
alert( result.input ); // Слава - это жажда молодости (вся поисковая строка)

У массива совпадений не всегда только один элемент.

Если часть шаблона выделяется скобками (...), то результат добавляется отдельным элементом в массиве.

Если скобки имеют наименование, обозначаемое как (?<name>...), то в result.groups[name] есть содержимое. Мы увидим это позже в главе о группах.

Например:

let str = "JavaScript - это такой язык";

let result = str.match( /JAVA(SCRIPT)/i );

alert( result[0] ); // JavaScript (всё совпадение полностью)
alert( result[1] ); // Script (часть совпадения, соответствующая скобкам)
alert( result.index ); // 0
alert( result.input ); // JavaScript - это такой язык

Из-за флага i поиск не чувствителен к регистру, поэтому он находит строку JavaScript. Часть соответствия, которая соответствует SCRIPT, становится отдельным элементом массива.

Таким образом, этот метод используется, чтобы найти одно полное соответствие со всеми деталями.

str.match(reg) с флагом «g»

Когда есть флаг "g", тогда str.match возвращает массив всех совпадений. В этом массиве нет дополнительных свойств, а круглые скобки не создают никаких элементов.

Например:

let str = "ОЙ-Ой-ой";

let result = str.match( /ой/ig );

alert( result ); // ОЙ, Ой, ой (массив из 3 совпадений, без учёта регистра)

Скобки ничего не меняют, поехали:

let str = "ОЙ-Ой-ой";

let result = str.match( /о(й)/ig );

alert( result ); // ОЙ, Ой, ой

Итак, с флагом g str.match возвращает простой массив всех совпадений без подробностей.

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

Если совпадений нет, str.match возвращает null

Пожалуйста, обратите внимание, это важно. Если совпадений нет, результатом является не пустой массив, а null.

Имейте это в виду, чтобы избежать ловушек, как это:

let str = "Ой-йой-йой";

alert(str.match(/лю/gi).length) // Ошибка! Нет свойства 'length' у null

Здесь str.match(/лю/gi) равно null, у него нет свойстваlength.

str.matchAll(regexp)

Метод str.matchAll(regexp) используется для поиска всех совпадений со всеми деталями.

Например:

let str = "Javascript или JavaScript? Нужны ли прописные буквы 'S'?";

let result = str.matchAll( /java(script)/ig );

let [match1, match2] = result;

alert( match1[0] ); // Javascript (Полное совпадение)
alert( match1[1] ); // script (Часть совпадения, которая соответсвует круглым скобкам)
alert( match1.index ); // 0
alert( match1.input ); // = str (Оригинальная строка целиком)

alert( match2[0] ); // JavaScript (Полное совпадение)
alert( match2[1] ); // Script (Часть совпадения, которая соответсвует круглым скобкам)
alert( match2.index ); // 15
alert( match2.input ); // = str (Оригинальная строка целиком)
matchAll возвращает итератор, а не массив

Например, если мы попытаемся получить первое совпадение по индексу, оно не будет работать:

let str = "Javascript or JavaScript??";

let result = str.matchAll( /javascript/ig );

alert(result[0]); // undefined (?! должно быть совпадение)

Причина в том, что итератор не является массивом. Нам нужно запустить на нем Array.from (result) или использовать цикл for..of для получения совпадений.

На практике, если нам нужны все совпадения, то for..of работает, так что это не проблема.

А, чтобы получить только несколько совпадений, мы можем использовать деструктуризацию:

let str = "Javascript or JavaScript??";

let [firstMatch] = str.matchAll( /javascript/ig );

alert(firstMatch); // Javascript
matchAll является новым, может потребоваться полифил

Метод может не работать в старых браузерах. Может потребоваться полифил (этот сайт использует core-js).

Или вы можете сделать цикл с помощью regexp.exec, как описано ниже.

str.split(regexp|substr, limit)

Разбивает строку в массив по разделителю – регулярному выражению regexp или подстроке substr.

Обычно мы используем метод split со строками, вот так:

alert('12-34-56'.split('-')) // массив [12, 34, 56]

Но мы можем разделить по регулярному выражению, таким же образом:

alert('12-34-56'.split(/-/)) // массив [12, 34, 56]

str.replace(str|reg, str|func)

Это универсальный метод поиска-и-замены, один из самых полезных. Этакий швейцарский армейский нож для поиска и замены.

Мы можем использовать его и без регулярных выражений, для поиска-и-замены подстроки:

// заменить тире двоеточием
alert('12-34-56'.replace("-", ":")) // 12:34-56

Хотя есть подводный камень.

Когда первый аргумент replace является строкой, он ищет только первое совпадение.

Вы можете видеть это в приведённом выше примере: только первый "-" заменяется на ":".

Чтобы найти все тире, нам нужно использовать не строку "-", а регулярное выражение /-/g с обязательным флагом g:

// заменить все тире двоеточием
alert( '12-34-56'.replace( /-/g, ":" ))  // 12:34:56

Второй аргумент – строка замены. Мы можем использовать специальные символы в нем:

Спецсимволы Действие в строке замены
$$ вставляет "$"
$& вставляет всё найденное совпадение
$` вставляет часть строки до совпадения
$' вставляет часть строки после совпадения
$n если n это 1-2 значное число, то это означает, что содержимое n-й скобки считается слева направо, в противном случае это означает круглую скобку с указанным именем

Например, если мы используем $& в строке замены, это означает «поместить все совпадение здесь».

Давайте используем его для добавления ко всем записям "John" строки "Mr.":

let str = "John Doe, John Smith and John Bull";

// для каждого John - заменить его на Mr., а затем добавить John
alert(str.replace(/John/g, 'Mr.$&'));  // Mr.John Doe, Mr.John Smith and Mr.John Bull

Довольно часто мы хотим повторно использовать части исходной строки, рекомбинировать их в замене или обернуть во что-нибудь.

Для этого мы должны:

  1. Отметить нужные части скобками в регулярном выражении.
  2. Использовать $1, $2 (и т.д.) в строке замены, чтобы получить содержимое, соответствующее 1-м, 2-м и так далее скобкам.

Например:

let str = "Афанасий Фет";

// поменять местами имя и фамилию
alert(str.replace(/(фет) (афанасий)/i, '$2, $1')) // Фет, Афанасий

Для ситуаций, которые требуют «умных» замен, вторым аргументом может быть функция.

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

Например:

let i = 0;

// заменить каждое "хо" на результат функции
alert("Хо-Хо-хо".replace(/хо/gi, function() {
  return ++i;
})); // 1-2-3

В приведённом выше примере функция просто возвращает следующий номер каждый раз, но обычно результат основан на совпадении.

Функция вызывается с аргументами func(str, p1, p2, ..., pn, offset, input, groups):

  1. str – найденное совпадение,
  2. p1, p2, ..., pn – содержимое скобок (если есть),
  3. offset – позиция, на которой найдено совпадение,
  4. input – исходная строка,
  5. groups – объект с именованными группами (см. главу Скобочные группы).

Если в регулярном выражении нет скобок, то есть только 3 аргумента: func(str, offset, input).

Давайте используем его, чтобы показать полную информацию о совпадениях:

// вывести и заменить все совпадения
function replacer(str, offset, input) {
  alert(`Найдено: ${str} на позиции: ${offset} в строке: ${input}`);
  return str.toLowerCase();
}

let result = "ОЙ-Ой-ой".replace(/ой/gi, replacer);
alert( 'Результат: ' + result ); // Результат: ой-ой-ой

// показывает каждое совпадение:
// Найдено: ОЙ на позиции: 0 в строке: ОЙ-Ой-ой
// Найдено: Ой на позиции: 3 в строке: ОЙ-Ой-ой
// Найдено: ой на позиции: 6 в строке: ОЙ-Ой-ой

В приведённом ниже примере есть две скобки, поэтому replacer вызывается с 5 аргументами: str – полное совпадение, затем круглые скобки, а затем offset иinput:

function replacer(str, name, surname, offset, input) {
  // name - первые скобки, surname - второе
  return surname + ", " + name;
}

let str = "Афанасий Фет";

alert(str.replace(/(Афанасий) (Фет)/, replacer)) // Фет, Афанасий

Использование функции даёт нам максимальные возможности по замене, потому что функция получает всю информацию о совпадении, имеет доступ к внешним переменным и может делать все что угодно.

regexp.exec(str)

Мы уже видели эти методы поиска:

  • search – ищет позицию совпадения,
  • match – если флаг g отсутствует, возвращает первое совпадение с круглыми скобками и всеми деталями,
  • match – если есть флаг g – возвращает все совпадения без подробных скобок,
  • matchAll – возвращает все совпадения с деталями.

Метод regexp.exec является наиболее гибким методом поиска из всех. В отличие от предыдущих методов, exec должен вызываться на регулярном выражении, а не на строке.

Он ведёт себя по-разному в зависимости от того, имеет ли регулярное выражение флаг g.

Если нет g, то regexp.exec(str) возвращает первое совпадение в точности как str.match(reg). Такое поведение не даёт нам ничего нового.

Но если есть g, то:

  • regexp.exec(str) возвращает первое совпадение и запоминает позицию после него в свойстве regexp.lastIndex.
  • Следующий вызов начинает поиск от regexp.lastIndex и возвращает следующее совпадение.
  • Если совпадений больше нет, то regexp.exec возвращает null, а для regexp.lastIndex устанавливается значение 0.

Мы могли бы использовать его, чтобы получить все совпадения с их позициями и группами скобок в цикле, вместо matchAll:

let str = 'Больше о JavaScript на https://javascript.info';

let regexp = /javascript/ig;

let result;

while (result = regexp.exec(str)) {
  alert( `Найдено ${result[0]} на позиции ${result.index}` );
  // показывает: Найдено JavaScript на позиции 9, затем
  // показывает: Найдено javascript на позиции 31
}

Конечно, matchAll делает то же самое, по крайней мере, для современных браузеров. Но то, что matchAll не может сделать – это поиск с заданной позиции.

Давайте искать с позиции 13. Нам нужно присвоить regexp.lastIndex=13 и вызватьregexp.exec:

let str = 'Больше о JavaScript на https://javascript.info';

let regexp = /javascript/ig;
regexp.lastIndex = 13;

let result;

while (result = regexp.exec(str)) {
  alert( `Найдено ${result[0]} на позиции ${result.index}` );
  // показывает: Found javascript at 31
}

Итак, начиная с заданной позиции 13, есть только одно совпадение.

regexp.test(str)

Метод regexp.test(str) ищет совпадение и возвращает true/false, в зависимости от того, находит ли он его.

Например:

let str = "Я люблю JavaScript";

// эти два теста делают одно и же
alert( /люблю/i.test(str) ); // true
alert( str.search(/люблю/i) != -1 ); // true

Пример с отрицательным ответом:

let str = "Ля-ля-ля";

alert( /люблю/i.test(str) ); // false
alert( str.search(/люблю/i) != -1 ); // false

Если регулярное выражение имеет флаг 'g', то regexp.test расширяется свойством regexp.lastIndex, точно так же, какregexp.exec.

Таким образом, мы можем использовать его для поиска с заданной позиции:

let regexp = /люблю/gi;

let str = "Я люблю JavaScript";

// начать поиск с 10 позиции:
regexp.lastIndex = 10
alert( regexp.test(str) ); // false (совпадений нет)
Одно и то же глобальное регулярное выражение, использованное повторно, может иметь другой результат

Если мы применяем одно и то же глобальное регулярное выражение последовательно к разным строкам, это может привести к неверному результату, поскольку вызов regexp.test обновляет свойство regexp.lastIndex, поэтому поиск в новой строке может начаться с ненулевой позиции.

Например, здесь мы дважды вызываем regexp.test для одного и того же текста, и второй раз поиск завершается уже неудачно:

let regexp = /javascript/g;  // (regexp только что создан: regexp.lastIndex=0)

alert( regexp.test("javascript") ); // true (а теперь regexp.lastIndex=10)
alert( regexp.test("javascript") ); // false

Это именно потому, что во втором тесте regexp.lastIndex не равен нулю.

Чтобы обойти это, можно использовать неглобальные регулярные выражения или переприсвоить regexp.lastIndex = 0 перед новым поиском.

Заключение

Существует множество методов как для регулярных выражений, так и для строк.

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

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

Карта учебника

Комментарии

перед тем как писать…
  • Если вам кажется, что в статье что-то не так - вместо комментария напишите на GitHub.
  • Для одной строки кода используйте тег <code>, для нескольких строк кода — тег <pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)
  • Если что-то непонятно в статье — пишите, что именно и с какого места.