Почему «asdf» .replace (/.*/ g, «x») == «xx»?

132

Я наткнулся на удивительный (для меня) факт.

console.log("asdf".replace(/.*/g, "x"));

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

рекурсивный
источник
9
более простой пример: "asdf".match(/.*/g)return ["asdf", ""]
Narro
32
Из-за глобального (g) флага. Глобальный флаг позволяет начать другой поиск в конце предыдущего совпадения, найдя пустую строку.
Цельсий
6
и давайте будем честными: наверное, никто не хотел именно такого поведения. Вероятно, это была деталь реализации желаемого "aa".replace(/b*/, "b")результата babab. И в какой-то момент мы стандартизировали все детали реализации веб-браузеров.
Люкс
4
@Joshua более старые версии GNU sed (не другие реализации!) Также демонстрировали эту ошибку, которая была исправлена ​​где-то между версиями 2.05 и 3.01 (20+ лет назад). Я подозреваю, что там, где это поведение возникло, прежде чем перейти в Perl (где оно стало функцией) и оттуда в JavaScript.
Мосви
1
@recursive - Достаточно справедливо. Я нахожу их обоих удивленными на секунду, потом понимаю "совпадение с нулевой шириной" и уже не удивляюсь. :-)
TJ Crowder

Ответы:

98

В соответствии со стандартом ECMA-262 String.prototype.replace вызывает RegExp.prototype [@@ replace] , что говорит:

11. Repeat, while done is false
  a. Let result be ? RegExpExec(rx, S).
  b. If result is null, set done to true.
  c. Else result is not null,
    i. Append result to the end of results.
    ii. If global is false, set done to true.
    iii. Else,
      1. Let matchStr be ? ToString(? Get(result, "0")).
      2. If matchStr is the empty String, then
        a. Let thisIndex be ? ToLength(? Get(rx, "lastIndex")).
        b. Let nextIndex be AdvanceStringIndex(S, thisIndex, fullUnicode).
        c. Perform ? Set(rx, "lastIndex", nextIndex, true).

где rxесть /.*/gи Sесть 'asdf'.

Смотрите 11.c.iii.2.b:

б. Пусть nextIndex будет AdvanceStringIndex (S, thisIndex, fullUnicode).

Поэтому в 'asdf'.replace(/.*/g, 'x')нем на самом деле:

  1. результат (не определено), результаты = [], lastIndex =0
  2. результат = 'asdf', результаты = [ 'asdf' ], lastIndex =4
  3. Результат = '', = результаты [ 'asdf', '' ], LastIndex = 4, AdvanceStringIndexустановите LastIndex к5
  4. результат = null, результаты = [ 'asdf', '' ], возврат

Поэтому есть 2 матча.

Алан Лян
источник
42
Этот ответ требует, чтобы я изучил это, чтобы понять это.
Фелипе
TL; DR состоит в том, что он соответствует 'asdf'и пустой строке ''.
Джим
34

Вместе в автономном чате с yawkat мы нашли интуитивно понятный способ понять "abcd".replace(/.*/g, "x"), почему именно получается два совпадения. Обратите внимание, что мы не проверили, полностью ли он соответствует семантике, наложенной стандартом ECMAScript, поэтому просто примите это как практическое правило.

Эмпирические правила

  • Рассматривайте совпадения как список кортежей (matchStr, matchIndex)в хронологическом порядке, которые указывают, какие части строки и индексы входной строки уже были съедены.
  • Этот список непрерывно строится, начиная слева от входной строки для регулярного выражения.
  • Части, уже съеденные, больше не могут быть сопоставлены
  • Замена выполняется по индексам, заданным путем matchIndexперезаписи подстроки matchStrв этой позиции. Если matchStr = "", то «замена» - это фактически вставка.

Формально акт сопоставления и замены описывается как цикл, как видно из другого ответа .

Простые примеры

  1. "abcd".replace(/.*/g, "x")выходы "xx":

    • Список матчей [("abcd", 0), ("", 4)]

      Примечательно, что он не включает следующие совпадения, о которых можно было подумать по следующим причинам:

      • ("a", 0), ("ab", 0): Квантор *жаден
      • ("b", 1), ("bc", 1): из-за предыдущего матча ("abcd", 0)строки "b"и "bc"уже съедены
      • ("", 4), ("", 4) (т.е. дважды): позиция индекса 4 уже съедена первым очевидным соответствием
    • Следовательно, замещающая строка "x"заменяет найденные совпадающие строки именно в этих позициях: в позиции 0 она заменяет строку, "abcd"а в позиции 4 она заменяет "".

      Здесь вы можете видеть, что замена может действовать как истинная замена предыдущей строки или просто как вставка новой строки.

  2. "abcd".replace(/.*?/g, "x")с ленивым*? выходом квантификатора"xaxbxcxdx"

    • Список матчей [("", 0), ("", 1), ("", 2), ("", 3), ("", 4)]

      В отличие от предыдущего примера, здесь ("a", 0), ("ab", 0), ("abc", 0)или даже ("abcd", 0)не включены из - за лени квантора, что строго ограничивает его , чтобы найти самый короткий матч.

    • Поскольку все совпадающие строки пусты, фактической замены не происходит, но вместо этого вставляются xв позиции 0, 1, 2, 3 и 4.

  3. "abcd".replace(/.+?/g, "x")с ленивым+? выходом квантификатора"xxxx"

    • Список матчей [("a", 0), ("b", 1), ("c", 2), ("d", 3)]
  4. "abcd".replace(/.{2,}?/g, "x")с ленивым[2,}? выходом квантификатора"xx"

    • Список матчей [("ab", 0), ("cd", 2)]
  5. "abcd".replace(/.{0}/g, "x")выходы "xaxbxcxdx"по той же логике, что и в примере 2.

Сложные примеры

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

  1. "abcdefgh".replace(/(?<=^(..)*)/g, "_"))с положительным просмотром назад(?<=...) выходами "_ab_cd_ef_gh_"(поддерживается только в Chrome до сих пор)

    • Список матчей [("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]
  2. "abcdefgh".replace(/(?=(..)*$)/g, "_"))с положительным прогнозным(?=...) выходом"_ab_cd_ef_gh_"

    • Список матчей [("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]
ComFreek
источник
4
Я думаю, что это довольно сложно назвать интуитивно понятным (и жирным шрифтом). Для меня это больше похоже на синдром Стокгольма и рационализацию. Ваш ответ хороший, кстати, я жалуюсь только на дизайн JS или отсутствие дизайна в этом отношении.
Эрик Думинил
7
@EricDuminil Сначала я тоже так думал, но после написания ответа набросанный алгоритм global-regex-replace кажется вполне подходящим, если бы он начинал с нуля. Это как while (!input not eaten up) { matchAndEat(); }. Кроме того, приведенные выше комментарии указывают на то, что это поведение возникло задолго до появления JavaScript.
ComFreek
2
Часть, которая все еще не имеет смысла (по любой другой причине, кроме «это то, что говорится в стандарте»), состоит в том, что совпадение из четырех символов ("abcd", 0)не занимает позицию 4, в которой должен идти следующий символ, но сопоставление с нулевым символом ("", 4)делает есть позиция 4, куда пойдет следующий символ. Если бы я проектировал это с нуля, я думаю, что я бы использовал правило, которое (str2, ix2)может следовать(str1, ix1) iff ix2 >= ix1 + str1.length() && ix2 + str2.length() > ix1 + str1.length(), что не вызывает эту ошибку.
Андерс Касеорг
2
@AndersKaseorg ("abcd", 0)не использует позицию 4, потому что "abcd"имеет длину всего 4 символа и, следовательно, просто ест индексы 0, 1, 2, 3. Я вижу, откуда могут исходить ваши рассуждения: почему мы не можем иметь ("abcd" ⋅ ε, 0)совпадение длиной 5 символов, где ⋅ такое конкатенация и εсовпадение с нулевой шириной? Формально потому что "abcd" ⋅ ε = "abcd". Я думал об интуитивной причине в последние минуты, но не смог ее найти. Я думаю, что всегда нужно рассматривать εкак происходящее само по себе "". Я хотел бы поиграть с альтернативной реализацией без этой ошибки или подвига. Не стесняйтесь поделиться!
ComFreek,
1
Если строка из четырех символов должна содержать четыре индекса, то строка с нулевым символом не должна содержать индексы. Любые рассуждения, которые вы могли бы сделать в отношении одного, должны в равной степени относиться и к другому (например "" ⋅ ε = "", хотя я не уверен, с каким различием вы собираетесь провести ""и чем ε, что означает одно и то же). Таким образом, разницу нельзя объяснить как интуитивную - она ​​просто есть.
Андерс Касеорг
26

Первый матч явно "asdf" (позиция [0,4]). Поскольку глобальный флаг ( g) установлен, поиск продолжается. В этой точке (позиция 4) он находит второе совпадение, пустую строку (позиция [4,4]).

Помните, что *соответствует нулю или более элементов.

Дэвид СК
источник
4
Так почему бы не три матча? Там может быть еще один пустой матч в конце. Их точно два. Это объяснение объясняет, почему может быть два, а не почему должно быть вместо одного или трех.
рекурсивный
7
Нет, другой пустой строки нет. Потому что эта пустая строка была найдена. пустая строка на позиции 4,4, обнаруживается как уникальный результат. Совпадение с меткой «4,4» не может быть повторено. возможно, вы можете подумать, что в позиции [0,0] есть пустая строка, но оператор * возвращает максимально возможное количество элементов. по этой причине возможно только 4,4
Дэвид С.К.
16
Мы должны помнить, что регулярные выражения не являются регулярными выражениями. В регулярных выражениях бесконечно много пустых строк между каждыми двумя символами, а также в начале и в конце. В регулярных выражениях пустых строк ровно столько, сколько указано в спецификации для конкретного вида движка регулярных выражений.
Йорг Миттаг
7
Это просто постфактум рационализация.
Мосви
9
@mosvy за исключением того, что это именно та логика, которая на самом деле используется.
Хоббс
1

просто, во-первых x, для замены соответствияasdf .

второй xдля пустой строки после asdf. Поиск прекращается, когда пусто.

Ниланка Маной
источник