Почему RegExp с глобальным флагом дает неправильные результаты?

277

В чем проблема с этим регулярным выражением, когда я использую глобальный флаг и флаг без учета регистра? Запрос - это пользовательский ввод. Результат должен быть [верно, верно].

var query = 'Foo B';
var re = new RegExp(query, 'gi');
var result = [];
result.push(re.test('Foo Bar'));
result.push(re.test('Foo Bar'));
// result will be [true, false]

var reg = /^a$/g;
for(i = 0; i++ < 10;)
   console.log(reg.test("a"));

около
источник
54
Добро пожаловать в одну из многих ловушек RegExp в JavaScript. У него один из худших интерфейсов для обработки регулярных выражений, который я когда-либо встречал, полный странных побочных эффектов и неясных предостережений. Большинство общих задач, которые вы обычно хотите выполнять с помощью регулярных выражений, трудно правильно написать.
bobince
XRegExp выглядит как хорошая альтернатива. xregexp.com
около
Смотрите также ответ здесь: stackoverflow.com/questions/604860/…
Prestaul
Одним из решений, если вы можете избежать неприятностей, является непосредственное использование литерала regex вместо его сохранения в re.
thdoan

Ответы:

350

RegExpОбъект отслеживает , lastIndexгде произошло совпадение, так и на последующих матчах он будет стартовать с последнего использованного индекса, вместо 0. Обратите внимание:

var query = 'Foo B';
var re = new RegExp(query, 'gi');
var result = [];
result.push(re.test('Foo Bar'));

alert(re.lastIndex);

result.push(re.test('Foo Bar'));

Если вы не хотите вручную сбрасывать lastIndexна 0 после каждого теста, просто уберите gфлаг.

Вот алгоритм, который диктуют спецификации (раздел 15.10.6.2):

RegExp.prototype.exec (строка)

Выполняет сопоставление регулярного выражения строки с регулярным выражением и возвращает объект Array, содержащий результаты совпадения, или ноль, если строка не совпадает. Строка ToString (string) ищется для вхождения шаблона регулярного выражения следующим образом:

  1. Пусть S будет значением ToString (string).
  2. Пусть длина будет длиной S.
  3. Пусть lastIndex будет значением свойства lastIndex.
  4. Позвольте мне быть значением ToInteger (lastIndex).
  5. Если глобальное свойство имеет значение false, пусть i = 0.
  6. Если I <0 или I> длина, тогда установите lastIndex в 0 и верните ноль.
  7. Вызовите [[Match]], передав ему аргументы S и i. Если [[Match]] вернул ошибку, перейдите к шагу 8; в противном случае пусть r будет результатом его состояния и перейдите к шагу 10.
  8. Пусть я = я + 1.
  9. Переходите к шагу 6.
  10. Пусть e будет значением endIndex для r.
  11. Если глобальное свойство имеет значение true, присвойте lastIndex значение e.
  12. Пусть n будет длиной массива захватов r. (Это то же значение, что и у NCapturingParens 15.10.2.1.)
  13. Вернуть новый массив со следующими свойствами:
    • Свойство index устанавливается в положение совпавшей подстроки в полной строке S.
    • Свойство ввода установлено в S.
    • Свойство длины имеет значение n + 1.
    • Свойство 0 устанавливается на совпавшую подстроку (то есть часть S между смещением i включительно и смещением e исключительно).
    • Для каждого целого числа i, такого, что I> 0 и I ≤ n, задайте для свойства с именем ToString (i) i-й элемент массива захвата r.
Ionuț G. Stan
источник
83
Это похоже на Руководство Автостопщика по разработке API Galaxy здесь. «Эта ловушка, в которую вы попали, была отлично задокументирована в спецификации в течение нескольких лет, если вы только удосужились проверить»,
Рецам
5
Липкий флаг Firefox не делает то, что вы подразумеваете вообще. Скорее, он действует так, как если бы в начале регулярного выражения был символ ^, ЗА ИСКЛЮЧЕНИЕМ того, что этот символ ^ соответствует текущей позиции строки (lastIndex), а не началу строки. Вы эффективно проверяете, соответствует ли регулярное выражение «прямо здесь» вместо «где-нибудь после lastIndex». Смотрите ссылку, которую вы предоставили!
делаешь
1
Вступительное утверждение этого ответа просто не точно. Вы выделили шаг 3 спецификации, которая ничего не говорит. Фактическое влияние lastIndexв шагах 5, 6 и 11. Ваше вступительное заявление верно только в том случае, если установлен глобальный флаг.
Prestaul
@ Prestaul да, вы правы, что в нем не упоминается глобальный флаг. Это было вероятно (не могу вспомнить, что я тогда думал) неявным из-за того, как вопрос сформулирован. Не стесняйтесь редактировать или удалять его и ссылаться на свой ответ. Кроме того, позвольте мне заверить вас, что вы лучше меня. Наслаждайтесь!
Ionuț G. Stan
@ IonuțG.Stan, извините, если мой предыдущий комментарий показался мне оскорбительным, это не было моей целью. Я не могу отредактировать это в данный момент, но я не пытался кричать, просто чтобы привлечь внимание к основному пункту моего комментария. Виноват!
Prestaul
72

Вы используете один RegExpобъект и выполняете его несколько раз. При каждом последующем выполнении он продолжается с последнего индекса соответствия.

Вам необходимо «сбросить» регулярное выражение для начала с начала перед каждым выполнением:

result.push(re.test('Foo Bar'));
re.lastIndex = 0;
result.push(re.test('Foo Bar'));
// result is now [true, true]

Сказав, что может быть более удобочитаемым создание нового объекта RegExp каждый раз (издержки минимальны, поскольку RegExp все равно кэшируется):

result.push((/Foo B/gi).test(stringA));
result.push((/Foo B/gi).test(stringB));
Роатин Март
источник
1
Или просто не используйте gфлаг.
Мельпомена
36

RegExp.prototype.testобновляет lastIndexсвойство регулярных выражений, чтобы каждый тест начинался там, где остановился последний. Я бы предложил использовать, String.prototype.matchпоскольку он не обновляет lastIndexсвойство:

!!'Foo Bar'.match(re); // -> true
!!'Foo Bar'.match(re); // -> true

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

Кроме того, вы можете просто сбросить lastIndexсвойство:

result.push(re.test('Foo Bar'));
re.lastIndex = 0;
result.push(re.test('Foo Bar'));
Джеймс
источник
12

Снятие глобального gфлага решит вашу проблему.

var re = new RegExp(query, 'gi');

Должно быть

var re = new RegExp(query, 'i');
user2572074
источник
0

Использование флага / g указывает ему продолжить поиск после попадания.

Если совпадение выполнено успешно, метод exec () возвращает массив и обновляет свойства объекта регулярного выражения.

Перед первым поиском:

myRegex.lastIndex
//is 0

После первого поиска

myRegex.lastIndex
//is 8

Удалите g, и он завершает поиск после каждого вызова exec ().

Скотт Шлехтлайтнер
источник
ОП не использует exec.
Мельпомена
0

Вам нужно установить re.lastIndex = 0, потому что с помощью флага regex отслеживайте последнее найденное совпадение, поэтому test не пойдет для проверки той же строки, для этого вам нужно сделать re.lastIndex = 0

var query = 'Foo B';
var re = new RegExp(query, 'gi');
var result = [];
result.push(re.test('Foo Bar'));
re.lastIndex=0;
result.push(re.test('Foo Bar'));

console.log(result)

Ashish
источник
-1

У меня была функция:

function parseDevName(name) {
  var re = /^([^-]+)-([^-]+)-([^-]+)$/g;
  var match = re.exec(name);
  return match.slice(1,4);
}

var rv = parseDevName("BR-H-01");
rv = parseDevName("BR-H-01");

Первый звонок работает. Второй звонок нет. sliceОперация жалуется на нулевое значение. Я предполагаю, что это из-за re.lastIndex. Это странно, потому что я ожидал новогоRegExp будет назначаться каждый раз, когда вызывается функция, а не совместно использоваться несколькими вызовами моей функции.

Когда я изменил это на:

var re = new RegExp('^([^-]+)-([^-]+)-([^-]+)$', 'g');

Тогда я не получаю lastIndexэффект удержания. Это работает так, как я ожидал.

Chelmite
источник