Чому "asdf" .replace (/.*/ g, "x") == "xx"?


129

Я натрапив на дивовижний (для мене) факт.

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

Чому дві заміни? Здається, будь-яка не порожня рядок без нових рядків створить рівно дві заміни для цього шаблону. Використовуючи функцію заміни, я бачу, що перша заміна призначена для всього рядка, а друга - для порожнього рядка.


9
більш простий приклад: "asdf".match(/.*/g)return ["asdf", ""]
Нарро

32
Через глобальний (g) прапор. Глобальний прапор дозволяє інший пошук розпочатись наприкінці попереднього матчу, знаходячи таким чином порожній рядок.
Цельсій

6
і, чесно кажучи: напевно, ніхто не хотів саме такої поведінки. це, мабуть, була деталізацією реалізації бажаючих "aa".replace(/b*/, "b")досягти результату babab. І в якийсь момент ми стандартизували всі деталі реалізації веб-браузерів.
Люкс

4
@Joshua старіші версії GNU sed (не інші реалізації) також демонстрували цю помилку, яку було виправлено десь між 2.05 та 3.01 випусками (20+ років тому). Я підозрюю, що саме там походить така поведінка, перш ніж пробитися в perl (де вона стала функцією) і звідти в javascript.
mosvy

1
@recursive - досить справедливо. Я вважаю їх обох здивуючими на секунду, потім усвідомлюю "матчу нульової ширини" і більше не дивуюсь. :-)
TJ Crowder

Відповіді:


98

Відповідно до стандарту ECMA-262 , String.prototype.replace викликає RegExp.prototype [@@ substitu] , який говорить:

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. result = 'asdf', results = [ '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)]

4
Я думаю, що це трохи розтягнення, щоб називати це інтуїтивно зрозумілим (і жирним шрифтом). Для мене це більше схоже на Стокгольмський синдром та пост-раціональну раціоналізацію. Ваша відповідь хороша, BTW, я нарікаю лише на JS-дизайн або на відсутність дизайну з цього приводу.
Ерік Думініл

7
@EricDuminil Спочатку я теж думав про це, але після того, як написав відповідь, ескізний алгоритм заміни глобального регексу, здається, саме такий, як можна було б придумати його, якби почати з нуля. Це як 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 becaues "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
David SK

16
Треба пам’ятати, що регулярні вирази не є регулярними виразами. У регулярних виразах є нескінченно багато порожніх рядків між кожними двома символами, а також на початку та в кінці. У регексах є рівно стільки порожніх рядків, скільки специфікація для конкретного аромату двигуна регулярних висловлювань говорить, що є.
Йорг W Міттаг

7
Це лише пост-спеціальна раціоналізація.
mosvy

9
@mosvy за винятком того, що саме така логіка використовується.
варення
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.