Я наткнулся на удивительный (для меня) факт.
console.log("asdf".replace(/.*/g, "x"));
Почему две замены? Кажется, что любая непустая строка без перевода строки будет производить ровно две замены для этого шаблона. Используя функцию замены, я вижу, что первая замена предназначена для всей строки, а вторая - для пустой строки.
javascript
regex
рекурсивный
источник
источник
"asdf".match(/.*/g)
return ["asdf", ""]"aa".replace(/b*/, "b")
результатаbabab
. И в какой-то момент мы стандартизировали все детали реализации веб-браузеров.Ответы:
В соответствии со стандартом ECMA-262 String.prototype.replace вызывает RegExp.prototype [@@ replace] , что говорит:
где
rx
есть/.*/g
иS
есть'asdf'
.Смотрите 11.c.iii.2.b:
Поэтому в
'asdf'.replace(/.*/g, 'x')
нем на самом деле:[]
, lastIndex =0
'asdf'
, результаты =[ 'asdf' ]
, lastIndex =4
''
, = результаты[ 'asdf', '' ]
, LastIndex =4
,AdvanceStringIndex
установите LastIndex к5
null
, результаты =[ 'asdf', '' ]
, возвратПоэтому есть 2 матча.
источник
'asdf'
и пустой строке''
.Вместе в автономном чате с yawkat мы нашли интуитивно понятный способ понять
"abcd".replace(/.*/g, "x")
, почему именно получается два совпадения. Обратите внимание, что мы не проверили, полностью ли он соответствует семантике, наложенной стандартом ECMAScript, поэтому просто примите это как практическое правило.Эмпирические правила
(matchStr, matchIndex)
в хронологическом порядке, которые указывают, какие части строки и индексы входной строки уже были съедены.matchIndex
перезаписи подстрокиmatchStr
в этой позиции. ЕслиmatchStr = ""
, то «замена» - это фактически вставка.Формально акт сопоставления и замены описывается как цикл, как видно из другого ответа .
Простые примеры
"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 она заменяет""
.Здесь вы можете видеть, что замена может действовать как истинная замена предыдущей строки или просто как вставка новой строки.
"abcd".replace(/.*?/g, "x")
с ленивым*?
выходом квантификатора"xaxbxcxdx"
Список матчей
[("", 0), ("", 1), ("", 2), ("", 3), ("", 4)]
В отличие от предыдущего примера, здесь
("a", 0)
,("ab", 0)
,("abc", 0)
или даже("abcd", 0)
не включены из - за лени квантора, что строго ограничивает его , чтобы найти самый короткий матч.Поскольку все совпадающие строки пусты, фактической замены не происходит, но вместо этого вставляются
x
в позиции 0, 1, 2, 3 и 4."abcd".replace(/.+?/g, "x")
с ленивым+?
выходом квантификатора"xxxx"
[("a", 0), ("b", 1), ("c", 2), ("d", 3)]
"abcd".replace(/.{2,}?/g, "x")
с ленивым[2,}?
выходом квантификатора"xx"
[("ab", 0), ("cd", 2)]
"abcd".replace(/.{0}/g, "x")
выходы"xaxbxcxdx"
по той же логике, что и в примере 2.Сложные примеры
Мы можем последовательно использовать идею вставки вместо замены, если мы просто всегда сопоставляем пустую строку и контролируем положение, в котором такие совпадения происходят с нашим преимуществом. Например, мы можем создать регулярные выражения, соответствующие пустой строке в каждой четной позиции, чтобы вставить туда символ:
"abcdefgh".replace(/(?<=^(..)*)/g, "_"))
с положительным просмотром назад(?<=...)
выходами"_ab_cd_ef_gh_"
(поддерживается только в Chrome до сих пор)[("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]
"abcdefgh".replace(/(?=(..)*$)/g, "_"))
с положительным прогнозным(?=...)
выходом"_ab_cd_ef_gh_"
[("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]
источник
while (!input not eaten up) { matchAndEat(); }
. Кроме того, приведенные выше комментарии указывают на то, что это поведение возникло задолго до появления JavaScript.("abcd", 0)
не занимает позицию 4, в которой должен идти следующий символ, но сопоставление с нулевым символом("", 4)
делает есть позиция 4, куда пойдет следующий символ. Если бы я проектировал это с нуля, я думаю, что я бы использовал правило, которое(str2, ix2)
может следовать(str1, ix1)
iffix2 >= ix1 + str1.length() && ix2 + str2.length() > ix1 + str1.length()
, что не вызывает эту ошибку.("abcd", 0)
не использует позицию 4, потому что"abcd"
имеет длину всего 4 символа и, следовательно, просто ест индексы 0, 1, 2, 3. Я вижу, откуда могут исходить ваши рассуждения: почему мы не можем иметь("abcd" ⋅ ε, 0)
совпадение длиной 5 символов, где ⋅ такое конкатенация иε
совпадение с нулевой шириной? Формально потому что"abcd" ⋅ ε = "abcd"
. Я думал об интуитивной причине в последние минуты, но не смог ее найти. Я думаю, что всегда нужно рассматриватьε
как происходящее само по себе""
. Я хотел бы поиграть с альтернативной реализацией без этой ошибки или подвига. Не стесняйтесь поделиться!"" ⋅ ε = ""
, хотя я не уверен, с каким различием вы собираетесь провести""
и чемε
, что означает одно и то же). Таким образом, разницу нельзя объяснить как интуитивную - она просто есть.Первый матч явно
"asdf"
(позиция [0,4]). Поскольку глобальный флаг (g
) установлен, поиск продолжается. В этой точке (позиция 4) он находит второе совпадение, пустую строку (позиция [4,4]).Помните, что
*
соответствует нулю или более элементов.источник
просто, во-первых
x
, для замены соответствияasdf
.второй
x
для пустой строки послеasdf
. Поиск прекращается, когда пусто.источник