JavaScript für ungeduldige Programmierer (ES2022-Ausgabe)
Bitte unterstützen Sie dieses Buch: kaufen Sie es oder spenden Sie
(Werbung, bitte nicht blockieren.)

43 Reguläre Ausdrücke (RegExp)



  Verfügbarkeit von Funktionen

Sofern nicht anders angegeben, ist jede Funktion für reguläre Ausdrücke seit ES3 verfügbar.

43.1 Reguläre Ausdrücke erstellen

43.1.1 Literal vs. Konstruktor

Die beiden Hauptwege zur Erstellung regulärer Ausdrücke sind:

Beide regulären Ausdrücke haben die gleichen zwei Teile:

43.1.2 Reguläre Ausdrücke klonen und nicht-destruktiv modifizieren

Es gibt zwei Varianten des Konstruktors RegExp():

Die zweite Variante ist nützlich zum Klonen regulärer Ausdrücke, optional während der Modifikation. Flags sind unveränderlich und dies ist der einzige Weg, sie zu ändern – zum Beispiel:

function copyAndAddFlags(regExp, flagsToAdd='') {
  // The constructor doesn’t allow duplicate flags;
  // make sure there aren’t any:
  const newFlags = Array.from(
    new Set(regExp.flags + flagsToAdd)
  ).join('');
  return new RegExp(regExp, newFlags);
}
assert.equal(/abc/i.flags, 'i');
assert.equal(copyAndAddFlags(/abc/i, 'g').flags, 'gi');

43.2 Syntax

43.2.1 Syntaxzeichen

Auf der obersten Ebene eines regulären Ausdrucks sind die folgenden Syntaxzeichen besonders. Sie werden durch Voranstellen eines Backslash (\) maskiert.

\ ^ $ . * + ? ( ) [ ] { } |

In regulären Expressions-Literalen müssen wir Schrägstriche maskieren:

> /\//.test('/')
true

Im Argument von new RegExp() müssen wir keine Schrägstriche maskieren:

> new RegExp('/').test('/')
true

43.2.2 Grundlegende Atome

Atome sind die grundlegenden Bausteine regulärer Ausdrücke.

43.2.3 Unicode-Eigenschafts-Escapes [ES2018]

43.2.3.1 Unicode-Zeicheneigenschaften

Im Unicode-Standard hat jedes Zeichen Eigenschaften – Metadaten, die es beschreiben. Eigenschaften spielen eine wichtige Rolle bei der Definition der Natur eines Zeichens. Zitat aus dem Unicode-Standard, Abschn. 3.3, D3:

Die Semantik eines Zeichens wird durch seine Identität, normative Eigenschaften und sein Verhalten bestimmt.

Hier sind einige Beispiele für Eigenschaften:

43.2.3.2 Unicode-Eigenschafts-Escapes

Unicode-Eigenschafts-Escapes sehen so aus:

  1. \p{prop=value}: passt für alle Zeichen, deren Eigenschaft prop den Wert value hat.
  2. \P{prop=value}: passt für alle Zeichen, die keine Eigenschaft prop mit dem Wert value haben.
  3. \p{bin_prop}: passt für alle Zeichen, deren binäre Eigenschaft bin_prop True ist.
  4. \P{bin_prop}: passt für alle Zeichen, deren binäre Eigenschaft bin_prop False ist.

Kommentare

Beispiele

Weiterführende Lektüre

43.2.4 Zeichenklassen

Eine Zeichenklasse fasst Klassenbereiche in eckigen Klammern zusammen. Die Klassenbereiche geben eine Menge von Zeichen an:

Regeln für Klassenbereiche:

43.2.5 Gruppen

43.2.6 Quantifizierer

Standardmäßig sind alle folgenden Quantifizierer gierig (sie passen auf so viele Zeichen wie möglich):

Um sie geizig zu machen (damit sie so wenige Zeichen wie möglich passen), setzen Sie Fragezeichen (?) dahinter:

> /".*"/.exec('"abc"def"')[0]  // greedy
'"abc"def"'
> /".*?"/.exec('"abc"def"')[0] // reluctant
'"abc"'

43.2.7 Assertions

43.2.7.1 Lookahead-Assertions

Positive Lookahead: (?=«muster») passt, wenn muster das Folgende übereinstimmt.

Beispiel: Folgen von Kleinbuchstaben, denen ein X folgt.

> 'abcX def'.match(/[a-z]+(?=X)/g)
[ 'abc' ]

Beachten Sie, dass das X selbst nicht Teil des übereinstimmenden Teilstrings ist.

Negative Lookahead: (?!«muster») passt, wenn muster das Folgende nicht übereinstimmt.

Beispiel: Folgen von Kleinbuchstaben, denen kein X folgt.

> 'abcX def'.match(/[a-z]+(?!X)/g)
[ 'ab', 'def' ]
43.2.7.2 Lookbehind-Assertions [ES2018]

Positive Lookbehind: (?<=«muster») passt, wenn muster dem Vorhergehenden entspricht.

Beispiel: Folgen von Kleinbuchstaben, denen ein X vorausgeht.

> 'Xabc def'.match(/(?<=X)[a-z]+/g)
[ 'abc' ]

Negative Lookbehind: (?<!«muster») passt, wenn muster dem Vorhergehenden nicht entspricht.

Beispiel: Folgen von Kleinbuchstaben, denen kein X vorausgeht.

> 'Xabc def'.match(/(?<!X)[a-z]+/g)
[ 'bc', 'def' ]

Beispiel: Ersetze „.js“ durch „.html“, aber nicht in „Node.js“.

> 'Node.js: index.js and main.js'.replace(/(?<!Node)\.js/g, '.html')
'Node.js: index.html and main.html'

43.2.8 Disjunktion (|)

Vorsicht: Dieser Operator hat niedrige Priorität. Verwenden Sie bei Bedarf Gruppen:

43.3 Flags

Tabelle 21: Dies sind die von JavaScript unterstützten Flags für reguläre Ausdrücke.
Literal-Flag Eigenschaftsname ES Beschreibung
d hasIndices ES2022 Schaltet Trefferindizes ein
g global ES3 Mehrfach übereinstimmen
i ignoreCase ES3 Groß-/Kleinschreibung ignorieren
m multiline ES3 ^ und $ passen pro Zeile
s dotAll ES2018 Der Punkt passt auf Zeilenumbrüche
u unicode ES6 Unicode-Modus (empfohlen)
y sticky ES6 Keine Zeichen zwischen Treffern

Die folgenden Flags für reguläre Ausdrücke sind in JavaScript verfügbar (Tab. 21 bietet eine kompakte Übersicht)

43.3.1 Wie sollen reguläre Expressions-Flags geordnet werden?

Betrachten Sie den folgenden regulären Ausdruck: /“([^”]+)”/udg

In welcher Reihenfolge sollten wir seine Flags auflisten? Zwei Optionen sind:

  1. Alphabetische Reihenfolge: /dgu
  2. Nach Wichtigkeit (vermutlich ist /u am grundlegendsten usw.): /ugd

Da (2) nicht offensichtlich ist, ist (1) die bessere Wahl. JavaScript verwendet es auch für die RegExp-Eigenschaft .flags.

> /a/ismudgy.flags
'dgimsuy'

43.3.2 Flag: Unicode-Modus über /u

Das Flag /u schaltet einen speziellen Unicode-Modus für reguläre Ausdrücke ein. Dieser Modus ermöglicht mehrere Funktionen:

Die folgenden Unterabschnitte erklären den letzten Punkt genauer. Sie verwenden das folgende Unicode-Zeichen, um zu erklären, wann atomare Einheiten Unicode-Zeichen sind und wann JavaScript-Zeichen:

const codePoint = '🙂';
const codeUnits = '\uD83D\uDE42'; // UTF-16

assert.equal(codePoint, codeUnits); // same string!

Ich wechsle nur zwischen 🙂 und \uD83D\uDE42, um zu veranschaulichen, wie JavaScript Dinge sieht. Beide sind äquivalent und können in Strings und regulären Ausdrücken austauschbar verwendet werden.

43.3.2.1 Folge: Wir können Unicode-Zeichen in Zeichenklassen einfügen

Mit /u werden die beiden Code-Einheiten von 🙂 als einzelnes Zeichen behandelt.

> /^[🙂]$/u.test('🙂')
true

Ohne /u wird 🙂 als zwei Zeichen behandelt.

> /^[\uD83D\uDE42]$/.test('\uD83D\uDE42')
false
> /^[\uD83D\uDE42]$/.test('\uDE42')
true

Beachten Sie, dass ^ und $ erfordern, dass der Eingangsstring ein einzelnes Zeichen hat. Deshalb ist das erste Ergebnis false.

43.3.2.2 Folge: Der Punktoperator (.) passt für Unicode-Zeichen, nicht für JavaScript-Zeichen

Mit /u passt der Punktoperator für Unicode-Zeichen:

> '🙂'.match(/./gu).length
1

.match() mit /g gibt ein Array mit allen Treffern eines regulären Ausdrucks zurück.

Ohne /u passt der Punktoperator für JavaScript-Zeichen:

> '\uD83D\uDE80'.match(/./g).length
2
43.3.2.3 Folge: Quantifizierer gelten für Unicode-Zeichen, nicht für JavaScript-Zeichen

Mit /u gilt ein Quantifizierer für das gesamte vorhergehende Unicode-Zeichen:

> /^🙂{3}$/u.test('🙂🙂🙂')
true

Ohne /u gilt ein Quantifizierer nur für das vorhergehende JavaScript-Zeichen:

> /^\uD83D\uDE80{3}$/.test('\uD83D\uDE80\uDE80\uDE80')
true

43.4 Eigenschaften von regulären Expressions-Objekten

Bemerkenswert:

43.4.1 Flags als Eigenschaften

Jedes Flag für reguläre Ausdrücke existiert als Eigenschaft mit einem längeren, beschreibenderen Namen:

> /a/i.ignoreCase
true
> /a/.ignoreCase
false

Dies ist die vollständige Liste der Flag-Eigenschaften:

43.4.2 Andere Eigenschaften

Jeder reguläre Ausdruck hat auch die folgenden Eigenschaften:

43.5 Trefferobjekte (Match Objects)

Mehrere Methoden im Zusammenhang mit regulären Ausdrücken geben sogenannte Trefferobjekte zurück, um detaillierte Informationen über die Stellen zu liefern, an denen ein regulärer Ausdruck mit einem Eingangsstring übereinstimmt. Diese Methoden sind:

Hier ist ein Beispiel:

assert.deepEqual(
  /(a+)b/d.exec('ab aaab'),
  {
    0: 'ab',
    1: 'a',
    index: 0,
    input: 'ab aaab',
    groups: undefined,
    indices: {
      0: [0, 2],
      1: [0, 1],
      groups: undefined
    },
  }
);

Das Ergebnis von .exec() ist ein Trefferobjekt für den ersten Treffer mit den folgenden Eigenschaften:

43.5.1 Trefferindizes in Trefferobjekten [ES2022]

Trefferindizes sind eine Funktion von Trefferobjekten: Wenn wir sie über das Flag /d des regulären Ausdrucks (Eigenschaft .hasIndices) aktivieren, zeichnen sie die Start- und Endindizes auf, wo Gruppen erfasst wurden.

43.5.1.1 Trefferindizes für nummerierte Gruppen

So greifen wir auf die Erfassungen von nummerierten Gruppen zu:

const matchObj = /(a+)(b+)/d.exec('aaaabb');
assert.equal(
  matchObj[1], 'aaaa'
);
assert.equal(
  matchObj[2], 'bb'
);

Aufgrund des Flags /d des regulären Ausdrucks hat matchObj auch eine Eigenschaft .indices, die für jede nummerierte Gruppe aufzeichnet, wo sie im Eingangsstring erfasst wurde.

assert.deepEqual(
  matchObj.indices[1], [0, 4]
);
assert.deepEqual(
  matchObj.indices[2], [4, 6]
);
43.5.1.2 Trefferindizes für benannte Gruppen

Die Erfassungen benannter Gruppen werden so zugegriffen:

const matchObj = /(?<as>a+)(?<bs>b+)/d.exec('aaaabb');
assert.equal(
  matchObj.groups.as, 'aaaa');
assert.equal(
  matchObj.groups.bs, 'bb');

Ihre Indizes werden in matchObj.indices.groups gespeichert.

assert.deepEqual(
  matchObj.indices.groups.as, [0, 4]);
assert.deepEqual(
  matchObj.indices.groups.bs, [4, 6]);
43.5.1.3 Ein realistischeres Beispiel

Ein wichtiger Anwendungsfall für Trefferindizes sind Parser, die genau aufzeigen, wo sich ein syntaktischer Fehler befindet. Der folgende Code löst ein verwandtes Problem: Er zeigt an, wo der zitierte Inhalt beginnt und wo er endet (siehe Demonstration am Ende).

const reQuoted = /“([^”]+)”/dgu;
function pointToQuotedText(str) {
  const startIndices = new Set();
  const endIndices = new Set();
  for (const match of str.matchAll(reQuoted)) {
    const [start, end] = match.indices[1];
    startIndices.add(start);
    endIndices.add(end);
  }
  let result = '';
  for (let index=0; index < str.length; index++) {
    if (startIndices.has(index)) {
      result += '[';
    } else if (endIndices.has(index+1)) {
      result += ']';
    } else {
      result += ' ';
    }
  }
  return result;
}

assert.equal(
  pointToQuotedText(
    'They said “hello” and “goodbye”.'),
    '           [   ]       [     ]  '
);

43.6 Methoden zur Arbeit mit regulären Ausdrücken

43.6.1 Standardmäßig passen reguläre Ausdrücke überall in einem String

Standardmäßig passen reguläre Ausdrücke überall in einem String.

> /a/.test('__a__')
true

Wir können das ändern, indem wir Assertions wie ^ verwenden oder das Flag /y nutzen.

> /^a/.test('__a__')
false
> /^a/.test('a__')
true

43.6.2 regExp.test(str): Gibt es einen Treffer? [ES3]

Die Methode .test() des regulären Ausdrucks gibt true zurück, wenn regExp mit str übereinstimmt.

> /bc/.test('ABCD')
false
> /bc/i.test('ABCD')
true
> /\.mjs$/.test('main.mjs')
true

Bei .test() sollten wir normalerweise das Flag /g vermeiden. Wenn wir es verwenden, erhalten wir im Allgemeinen nicht jedes Mal dasselbe Ergebnis, wenn wir die Methode aufrufen.

> const r = /a/g;
> r.test('aab')
true
> r.test('aab')
true
> r.test('aab')
false

Die Ergebnisse ergeben sich daraus, dass /a/ zwei Treffer im String hat. Nachdem all diese gefunden wurden, gibt .test() false zurück.

43.6.3 str.search(regExp): An welchem Index ist der Treffer? [ES3]

Die String-Methode .search() gibt den ersten Index von str zurück, an dem ein Treffer für regExp vorhanden ist.

> '_abc_'.search(/abc/)
1
> 'main.mjs'.search(/\.mjs$/)
4

43.6.4 regExp.exec(str): Erfassungsgruppen [ES3]

43.6.4.1 Ein Trefferobjekt für den ersten Treffer erhalten

Ohne das Flag /g gibt .exec() ein Trefferobjekt für den ersten Treffer von regExp in str zurück.

assert.deepEqual(
  /(a+)b/.exec('ab aab'),
  {
    0: 'ab',
    1: 'a',
    index: 0,
    input: 'ab aab',
    groups: undefined,
  }
);
43.6.4.2 Benannte Erfassungsgruppen [ES2018]

Das vorherige Beispiel enthielt eine einzelne nummerierte Gruppe. Das folgende Beispiel demonstriert benannte Gruppen:

assert.deepEqual(
  /(?<as>a+)b/.exec('ab aab'),
  {
    0: 'ab',
    1: 'a',
    index: 0,
    input: 'ab aab',
    groups: { as: 'a' },
  }
);

Im Ergebnis von .exec() sehen wir, dass eine benannte Gruppe auch eine nummerierte Gruppe ist – ihre Erfassung existiert zweimal:

43.6.4.3 Über alle Treffer iterieren

  Bessere Alternative zum Abrufen aller Treffer: str.matchAll(regExp) [ES2020]

Seit ECMAScript 2020 gibt es in JavaScript eine weitere Methode zum Abrufen aller Treffer: str.matchAll(regExp). Diese Methode ist einfacher zu verwenden und hat weniger Fallstricke.

Wenn wir alle Treffer eines regulären Ausdrucks abrufen möchten (nicht nur den ersten), müssen wir das Flag /g aktivieren. Dann können wir .exec() mehrmals aufrufen und erhalten jedes Mal einen Treffer. Nach dem letzten Treffer gibt .exec() null zurück.

> const regExp = /(a+)b/g;
> regExp.exec('ab aab')
{ 0: 'ab', 1: 'a', index: 0, input: 'ab aab', groups: undefined }
> regExp.exec('ab aab')
{ 0: 'aab', 1: 'aa', index: 3, input: 'ab aab', groups: undefined }
> regExp.exec('ab aab')
null

Daher können wir über alle Treffer wie folgt iterieren:

const regExp = /(a+)b/g;
const str = 'ab aab';

let match;
// Check for null via truthiness
// Alternative: while ((match = regExp.exec(str)) !== null)
while (match = regExp.exec(str)) {
  console.log(match[1]);
}
// Output:
// 'a'
// 'aa'

  Seien Sie vorsichtig, wenn Sie reguläre Ausdrücke mit /g teilen!

Das Teilen von regulären Ausdrücken mit /g birgt einige Fallstricke, die später erklärt werden.

  Übung: Zitierten Text über .exec() extrahieren

exercises/regexps/extract_quoted_test.mjs

43.6.5 str.match(regExp): Alle Treffer der Gruppe 0 erhalten [ES3]

Ohne /g verhält sich .match() wie .exec() – es gibt ein einzelnes Trefferobjekt zurück.

Mit /g gibt .match() alle Teilstrings von str zurück, die mit regExp übereinstimmen:

> 'ab aab'.match(/(a+)b/g)
[ 'ab', 'aab' ]

Wenn kein Treffer vorhanden ist, gibt .match() null zurück.

> 'xyz'.match(/(a+)b/g)
null

Wir können den Nullish Coalescing Operator (??) verwenden, um uns vor null zu schützen.

const numberOfMatches = (str.match(regExp) ?? []).length;

43.6.6 str.matchAll(regExp): Ein Iterable über alle Trefferobjekte erhalten [ES2020]

So wird .matchAll() aufgerufen:

const matchIterable = str.matchAll(regExp);

Gegeben einen String und einen regulären Ausdruck, gibt .matchAll() ein Iterable über die Trefferobjekte aller Treffer zurück.

Im folgenden Beispiel verwenden wir Array.from(), um Iterables in Arrays umzuwandeln, damit wir sie besser vergleichen können.

> Array.from('-a-a-a'.matchAll(/-(a)/ug))
[
  { 0:'-a', 1:'a', index: 0, input: '-a-a-a', groups: undefined },
  { 0:'-a', 1:'a', index: 2, input: '-a-a-a', groups: undefined },
  { 0:'-a', 1:'a', index: 4, input: '-a-a-a', groups: undefined },
]

Flag /g muss gesetzt sein:

> Array.from('-a-a-a'.matchAll(/-(a)/u))
TypeError: String.prototype.matchAll called with a non-global
RegExp argument

.matchAll() wird von regExp.lastIndex nicht beeinflusst und ändert es auch nicht.

43.6.6.1 Implementierung von .matchAll()

.matchAll() könnte über .exec() wie folgt implementiert werden:

function* matchAll(str, regExp) {
  if (!regExp.global) {
    throw new TypeError('Flag /g must be set!');
  }
  const localCopy = new RegExp(regExp, regExp.flags);
  let match;
  while (match = localCopy.exec(str)) {
    yield match;
  }
}

Eine lokale Kopie zu erstellen, stellt zwei Dinge sicher:

Verwendung von matchAll()

const str = '"fee" "fi" "fo" "fum"';
const regex = /"([^"]*)"/g;

for (const match of matchAll(str, regex)) {
  console.log(match[1]);
}
// Output:
// 'fee'
// 'fi'
// 'fo'
// 'fum'

43.6.7 regExp.exec() vs. str.match() vs. str.matchAll()

Die folgende Tabelle fasst die Unterschiede zwischen den drei Methoden zusammen:

Ohne /g Mit /g
regExp.exec(str) Erstes Trefferobjekt Nächstes Trefferobjekt oder null
str.match(regExp) Erstes Trefferobjekt Array von Gruppenerfassungen (Gruppe 0)
str.matchAll(regExp) TypeError Iterable über Trefferobjekte

43.6.8 Ersetzen mit str.replace() und str.replaceAll()

Beide Ersetzungsmethoden haben zwei Parameter:

searchValue kann sein:

replacementValue kann sein:

Die beiden Methoden unterscheiden sich wie folgt:

Diese Tabelle fasst zusammen, wie das funktioniert:

Suchen nach: string RegExp ohne /g RegExp mit /g
.replace Erstes Vorkommen Erstes Vorkommen (Alle Vorkommen)
.replaceAll Alle Vorkommen TypeError Alle Vorkommen

Die letzte Spalte von .replace() steht in Klammern, weil diese Methode lange vor .replaceAll() existierte und daher Funktionalität unterstützt, die jetzt über letztere Methode gehandhabt werden sollte. Wenn wir das ändern könnten, würde .replace() hier eine TypeError auslösen.

Wir untersuchen zuerst, wie .replace() und .replaceAll() einzeln funktionieren, wenn replacementValue ein einfacher String ist (ohne das Zeichen $). Dann untersuchen wir, wie beide von komplexeren Ersetzungswerten beeinflusst werden.

43.6.8.1 str.replace(searchValue, replacementValue) [ES3]

Wie .replace() arbeitet, wird von seinem ersten Parameter searchValue beeinflusst:

Wenn wir jedes Vorkommen eines Strings ersetzen wollen, haben wir zwei Optionen:

43.6.8.2 str.replaceAll(searchValue, replacementValue) [ES2021]

Wie .replaceAll() arbeitet, wird von seinem ersten Parameter searchValue beeinflusst:

43.6.8.3 Der Parameter replacementValue von .replace() und .replaceAll()

Bisher haben wir den Parameter replacementValue nur mit einfachen Strings verwendet, aber er kann mehr. Wenn sein Wert ist:

43.6.8.4 replacementValue ist ein String

Wenn der Ersatzwert ein String ist, hat das Dollarzeichen eine besondere Bedeutung – es fügt Text ein, der vom regulären Ausdruck gefunden wurde:

Text Ergebnis
$$ einzelnes $
$& vollständiger Treffer
$` Text vor dem Treffer
$' Text nach dem Treffer
$n Erfassung der nummerierten Gruppe n (n > 0)
$<name> Erfassung der benannten Gruppe name [ES2018]

Beispiel: Einfügen des Texts vor, während und nach dem übereinstimmenden Teilstring.

> 'a1 a2'.replaceAll(/a/g, "($`|$&|$')")
'(|a|1 a2)1 (a1 |a|2)2'

Beispiel: Einfügen der Erfassungen von nummerierten Gruppen.

> const regExp = /^([A-Za-z]+): (.*)$/ug;
> 'first: Jane'.replaceAll(regExp, 'KEY: $1, VALUE: $2')
'KEY: first, VALUE: Jane'

Beispiel: Einfügen der Erfassungen von benannten Gruppen.

> const regExp = /^(?<key>[A-Za-z]+): (?<value>.*)$/ug;
> 'first: Jane'.replaceAll(regExp, 'KEY: $<key>, VALUE: $<value>')
'KEY: first, VALUE: Jane'

  Übung: Anführungszeichen über .replace() und eine benannte Gruppe ändern

exercises/regexps/change_quotes_test.mjs

43.6.8.5 replacementValue ist eine Funktion

Wenn der Ersetzungswert eine Funktion ist, können wir jede Ersetzung berechnen. Im folgenden Beispiel multiplizieren wir jede gefundene nicht-negative Ganzzahl mit zwei.

assert.equal(
  '3 cats and 4 dogs'.replaceAll(/[0-9]+/g, (all) => 2 * Number(all)),
  '6 cats and 8 dogs'
);

Die Ersetzungsfunktion erhält die folgenden Parameter. Beachten Sie, wie ähnlich sie den Trefferobjekten sind. Diese Parameter sind alle positionsbezogen, aber ich habe hinzugefügt, wie man sie benennen könnte

Wenn uns nur groups interessiert, können wir folgende Technik verwenden

const result = 'first=jane, last=doe'.replace(
  /(?<key>[a-z]+)=(?<value>[a-z]+)/g,
  (...args) => { // (A)
    const groups = args.at(-1); // (B)
    const {key, value} = groups;
    return key.toUpperCase() + '=' + value.toUpperCase();
  });
assert.equal(result, 'FIRST=JANE, LAST=DOE');

Aufgrund des Rest-Parameters in Zeile A enthält args ein Array mit allen Parametern. Wir greifen über die Array-Methode .at() in Zeile B auf den letzten Parameter zu.

43.6.9 Andere Methoden zur Arbeit mit regulären Ausdrücken

String.prototype.split() wird im Kapitel über Strings beschrieben. Sein erster Parameter von String.prototype.split() ist entweder ein String oder ein regulärer Ausdruck. Wenn letzteres der Fall ist, erscheinen Erfassungen von Gruppen im Ergebnis

> 'a:b : c'.split(':')
[ 'a', 'b ', ' c' ]
> 'a:b : c'.split(/ *: */)
[ 'a', 'b', 'c' ]
> 'a:b : c'.split(/( *):( *)/)
[ 'a', '', '', 'b', ' ', ' ', 'c' ]

43.7 Die Flags /g und /y und die Eigenschaft .lastIndex (fortgeschritten)

In diesem Abschnitt untersuchen wir, wie die RegExp-Flags /g und /y funktionieren und wie sie von der RegExp-Eigenschaft .lastIndex abhängen. Wir werden auch einen interessanten Anwendungsfall für .lastIndex entdecken, der Sie überraschen dürfte.

43.7.1 Die Flags /g und /y

Jede Methode reagiert anders auf /g und /y; dies gibt uns eine grobe allgemeine Vorstellung

Wenn ein regulärer Ausdruck weder das Flag /g noch das Flag /y hat, erfolgt die Übereinstimmung einmal und beginnt am Anfang.

Mit entweder /g oder /y erfolgt die Übereinstimmung relativ zu einer "aktuellen Position" innerhalb des Eingabe-Strings. Diese Position wird in der Eigenschaft .lastIndex des regulären Ausdrucks gespeichert.

Es gibt drei Gruppen von regulären Ausdruck-bezogenen Methoden

  1. Die String-Methoden .search(regExp) und .split(regExp) ignorieren /g und /y (und damit auch .lastIndex) vollständig.

  2. Die RegExp-Methoden .exec(str) und .test(str) ändern sich auf zwei Arten, wenn entweder /g oder /y gesetzt ist.

    Erstens erhalten wir mehrere Treffer, indem wir eine Methode wiederholt aufrufen. Jedes Mal gibt sie entweder ein weiteres Ergebnis (ein Trefferobjekt oder true) oder einen "Ende der Ergebnisse"-Wert (null oder false) zurück.

    Zweitens wird die Eigenschaft .lastIndex des regulären Ausdrucks verwendet, um sich durch den Eingabe-String zu bewegen. Einerseits bestimmt .lastIndex, wo die Übereinstimmung beginnt

    • /g bedeutet, dass eine Übereinstimmung bei .lastIndex oder später beginnen muss.

    • /y bedeutet, dass eine Übereinstimmung bei .lastIndex beginnen muss. Das heißt, der Anfang des regulären Ausdrucks ist an .lastIndex gebunden.

      Beachten Sie, dass ^ und $ weiterhin wie gewohnt funktionieren: Sie binden Treffer an den Anfang oder das Ende des Eingabe-Strings, es sei denn, .multiline ist gesetzt. Dann binden sie an den Anfang oder das Ende von Zeilen.

    Andererseits wird .lastIndex auf eins plus den letzten Index des vorherigen Treffers gesetzt.

  3. Alle anderen Methoden werden wie folgt beeinflusst

    • /g führt zu mehreren Treffern.
    • /y führt zu einem einzelnen Treffer, der bei .lastIndex beginnen muss.
    • /yg führt zu mehreren Treffern ohne Lücken.

Dies war eine erste Übersicht. Die nächsten Abschnitte gehen auf weitere Details ein.

43.7.2 Wie genau werden Methoden von /g und /y beeinflusst?

43.7.2.1 regExp.exec(str) [ES3]

Ohne /g und /y ignoriert .exec() .lastIndex und gibt immer ein Trefferobjekt für den ersten Treffer zurück

> const re = /#/; re.lastIndex = 1;
> [re.exec('##-#'), re.lastIndex]
[{ 0: '#', index: 0, input: '##-#' }, 1]
> [re.exec('##-#'), re.lastIndex]
[{ 0: '#', index: 0, input: '##-#' }, 1]

Mit /g muss der Treffer bei .lastIndex oder später beginnen. .lastIndex wird aktualisiert. Wenn es keinen Treffer gibt, wird null zurückgegeben.

> const re = /#/g; re.lastIndex = 1;
> [re.exec('##-#'), re.lastIndex]
[{ 0: '#', index: 1, input: '##-#' }, 2]
> [re.exec('##-#'), re.lastIndex]
[{ 0: '#', index: 3, input: '##-#' }, 4]
> [re.exec('##-#'), re.lastIndex]
[null, 0]

Mit /y muss der Treffer exakt bei .lastIndex beginnen. .lastIndex wird aktualisiert. Wenn es keinen Treffer gibt, wird null zurückgegeben.

> const re = /#/y; re.lastIndex = 1;
> [re.exec('##-#'), re.lastIndex]
[{ 0: '#', index: 1, input: '##-#' }, 2]
> [re.exec('##-#'), re.lastIndex]
[null, 0]

Mit /yg verhält sich .exec() genauso wie mit /y.

43.7.2.2 regExp.test(str) [ES3]

Diese Methode verhält sich genauso wie .exec(), gibt aber anstelle eines Trefferobjekts true und anstelle von null false zurück.

Zum Beispiel ist ohne /g oder /y das Ergebnis immer true

> const re = /#/; re.lastIndex = 1;
> [re.test('##-#'), re.lastIndex]
[true, 1]
> [re.test('##-#'), re.lastIndex]
[true, 1]

Mit /g gibt es zwei Treffer

> const re = /#/g; re.lastIndex = 1;
> [re.test('##-#'), re.lastIndex]
[true, 2]
> [re.test('##-#'), re.lastIndex]
[true, 4]
> [re.test('##-#'), re.lastIndex]
[false, 0]

Mit /y gibt es nur einen Treffer

> const re = /#/y; re.lastIndex = 1;
> [re.test('##-#'), re.lastIndex]
[true, 2]
> [re.test('##-#'), re.lastIndex]
[false, 0]

Mit /yg verhält sich .test() genauso wie mit /y.

43.7.2.3 str.match(regExp) [ES3]

Ohne /g funktioniert .match() wie .exec(). Entweder ohne /y

> const re = /#/; re.lastIndex = 1;
> ['##-#'.match(re), re.lastIndex]
[{ 0: '#', index: 0, input: '##-#' }, 1]
> ['##-#'.match(re), re.lastIndex]
[{ 0: '#', index: 0, input: '##-#' }, 1]

Oder mit /y

> const re = /#/y; re.lastIndex = 1;
> ['##-#'.match(re), re.lastIndex]
[{ 0: '#', index: 1, input: '##-#' }, 2]
> ['##-#'.match(re), re.lastIndex]
[null, 0]

Mit /g erhalten wir alle Treffer (Gruppe 0) in einem Array. .lastIndex wird ignoriert und auf Null zurückgesetzt.

> const re = /#/g; re.lastIndex = 1;
> '##-#'.match(re)
['#', '#', '#']
> re.lastIndex
0

/yg funktioniert wie /g, aber es gibt keine Lücken zwischen den Treffern

> const re = /#/yg; re.lastIndex = 1;
> '##-#'.match(re)
['#', '#']
> re.lastIndex
0
43.7.2.4 str.matchAll(regExp) [ES2020]

Wenn /g nicht gesetzt ist, löst .matchAll() eine Ausnahme aus

> const re = /#/y; re.lastIndex = 1;
> '##-#'.matchAll(re)
TypeError: String.prototype.matchAll called with
a non-global RegExp argument

Wenn /g gesetzt ist, beginnt die Übereinstimmung bei .lastIndex und diese Eigenschaft wird nicht geändert

> const re = /#/g; re.lastIndex = 1;
> Array.from('##-#'.matchAll(re))
[
  { 0: '#', index: 1, input: '##-#' },
  { 0: '#', index: 3, input: '##-#' },
]
> re.lastIndex
1

Wenn /yg gesetzt ist, ist das Verhalten dasselbe wie bei /g, aber es gibt keine Lücken zwischen den Treffern

> const re = /#/yg; re.lastIndex = 1;
> Array.from('##-#'.matchAll(re))
[
  { 0: '#', index: 1, input: '##-#' },
]
> re.lastIndex
1
43.7.2.5 str.replace(regExp, str) [ES3]

Ohne /g und /y wird nur das erste Vorkommen ersetzt

> const re = /#/; re.lastIndex = 1;
> '##-#'.replace(re, 'x')
'x#-#'
> re.lastIndex
1

Mit /g werden alle Vorkommen ersetzt. .lastIndex wird ignoriert, aber auf Null zurückgesetzt.

> const re = /#/g; re.lastIndex = 1;
> '##-#'.replace(re, 'x')
'xx-x'
> re.lastIndex
0

Mit /y wird nur das (erste) Vorkommen bei .lastIndex ersetzt. .lastIndex wird aktualisiert.

> const re = /#/y; re.lastIndex = 1;
> '##-#'.replace(re, 'x')
'#x-#'
> re.lastIndex
2

/yg funktioniert wie /g, aber Lücken zwischen Treffern sind nicht erlaubt

> const re = /#/yg; re.lastIndex = 1;
> '##-#'.replace(re, 'x')
'xx-#'
> re.lastIndex
0
43.7.2.6 str.replaceAll(regExp, str) [ES2021]

.replaceAll() funktioniert wie .replace(), löst aber eine Ausnahme aus, wenn /g nicht gesetzt ist

> const re = /#/y; re.lastIndex = 1;
> '##-#'.replaceAll(re, 'x')
TypeError: String.prototype.replaceAll called
with a non-global RegExp argument

43.7.3 Vier Fallstricke von /g und /y und wie man mit ihnen umgeht

Wir werden uns zuerst vier Fallstricke von /g und /y ansehen und dann Wege, um mit diesen Fallstricken umzugehen.

43.7.3.1 Fallstrick 1: Wir können keinen regulären Ausdruck mit /g oder /y inline einfügen

Ein regulärer Ausdruck mit /g kann nicht inline verwendet werden. Zum Beispiel wird in der folgenden while-Schleife der reguläre Ausdruck jedes Mal, wenn die Bedingung geprüft wird, neu erstellt. Daher ist sein .lastIndex immer Null und die Schleife endet nie.

let matchObj;
// Infinite loop
while (matchObj = /a+/g.exec('bbbaabaaa')) {
  console.log(matchObj[0]);
}

Mit /y ist das Problem dasselbe.

43.7.3.2 Fallstrick 2: Das Entfernen von /g oder /y kann Code brechen

Wenn Code einen regulären Ausdruck mit /g erwartet und eine Schleife über die Ergebnisse von .exec() oder .test() hat, kann ein regulärer Ausdruck ohne /g eine Endlosschleife verursachen

function collectMatches(regExp, str) {
  const matches = [];
  let matchObj;
  // Infinite loop
  while (matchObj = regExp.exec(str)) {
    matches.push(matchObj[0]);
  }
  return matches;
}
collectMatches(/a+/, 'bbbaabaaa'); // Missing: flag /g

Warum gibt es eine Endlosschleife? Weil .exec() immer das erste Ergebnis, ein Trefferobjekt, zurückgibt und niemals null.

Mit /y ist das Problem dasselbe.

43.7.3.3 Fallstrick 3: Das Hinzufügen von /g oder /y kann Code brechen

Bei .test() gibt es einen weiteren Vorbehalt: Es wird von .lastIndex beeinflusst. Daher muss, wenn wir genau einmal prüfen wollen, ob ein regulärer Ausdruck mit einem String übereinstimmt, der reguläre Ausdruck kein /g haben. Andernfalls erhalten wir im Allgemeinen bei jedem Aufruf von .test() ein anderes Ergebnis

> const regExp = /^X/g;
> [regExp.test('Xa'), regExp.lastIndex]
[ true, 1 ]
> [regExp.test('Xa'), regExp.lastIndex]
[ false, 0 ]
> [regExp.test('Xa'), regExp.lastIndex]
[ true, 1 ]

Die erste Invocage liefert einen Treffer und aktualisiert .lastIndex. Die zweite Invocage findet keinen Treffer und setzt .lastIndex auf Null zurück.

Wenn wir einen regulären Ausdruck speziell für .test() erstellen, werden wir wahrscheinlich kein /g hinzufügen. Die Wahrscheinlichkeit, auf /g zu stoßen, steigt jedoch, wenn wir denselben regulären Ausdruck zum Ersetzen und zum Testen verwenden.

Auch hier besteht dieses Problem auch mit /y

> const regExp = /^X/y;
> regExp.test('Xa')
true
> regExp.test('Xa')
false
> regExp.test('Xa')
true
43.7.3.4 Fallstrick 4: Code kann unerwartete Ergebnisse liefern, wenn .lastIndex nicht Null ist

Angesichts all der Operationen mit regulären Ausdrücken, die von .lastIndex beeinflusst werden, müssen wir bei vielen Algorithmen darauf achten, dass .lastIndex am Anfang Null ist. Andernfalls können wir unerwartete Ergebnisse erhalten

function countMatches(regExp, str) {
  let count = 0;
  while (regExp.test(str)) {
    count++;
  }
  return count;
}

const myRegExp = /a/g;
myRegExp.lastIndex = 4;
assert.equal(
  countMatches(myRegExp, 'babaa'), 1); // should be 3

Normalerweise ist .lastIndex bei neu erstellten regulären Ausdrücken Null und wir ändern ihn nicht explizit, wie wir es im Beispiel getan haben. Aber .lastIndex kann immer noch nicht Null sein, wenn wir den regulären Ausdruck mehrmals verwenden.

43.7.3.5 Wie man die Fallstricke von /g und /y vermeidet

Als Beispiel für den Umgang mit /g und .lastIndex greifen wir countMatches() aus dem vorherigen Beispiel wieder auf. Wie verhindern wir, dass ein falscher regulärer Ausdruck unseren Code bricht? Schauen wir uns drei Ansätze an.

43.7.3.5.1 Ausnahmen auslösen

Erstens können wir eine Ausnahme auslösen, wenn /g nicht gesetzt ist oder .lastIndex nicht Null ist

function countMatches(regExp, str) {
  if (!regExp.global) {
    throw new Error('Flag /g of regExp must be set');
  }
  if (regExp.lastIndex !== 0) {
    throw new Error('regExp.lastIndex must be zero');
  }
  
  let count = 0;
  while (regExp.test(str)) {
    count++;
  }
  return count;
}
43.7.3.5.2 Reguläre Ausdrücke klonen

Zweitens können wir den Parameter klonen. Das hat den zusätzlichen Vorteil, dass regExp nicht verändert wird.

function countMatches(regExp, str) {
  const cloneFlags = regExp.flags + (regExp.global ? '' : 'g');
  const clone = new RegExp(regExp, cloneFlags);

  let count = 0;
  while (clone.test(str)) {
    count++;
  }
  return count;
}
43.7.3.5.3 Eine Operation verwenden, die nicht von .lastIndex oder Flags beeinflusst wird

Mehrere Operationen mit regulären Ausdrücken werden nicht von .lastIndex oder von Flags beeinflusst. Zum Beispiel ignoriert .match() .lastIndex, wenn /g vorhanden ist

function countMatches(regExp, str) {
  if (!regExp.global) {
    throw new Error('Flag /g of regExp must be set');
  }
  return (str.match(regExp) ?? []).length;
}

const myRegExp = /a/g;
myRegExp.lastIndex = 4;
assert.equal(countMatches(myRegExp, 'babaa'), 3); // OK!

Hier funktioniert countMatches(), auch wenn wir .lastIndex nicht geprüft oder korrigiert haben.

43.7.4 Anwendungsfall für .lastIndex: Übereinstimmung an einem bestimmten Index starten

Neben der Speicherung von Zuständen kann .lastIndex auch verwendet werden, um die Übereinstimmung an einem bestimmten Index zu starten. Dieser Abschnitt beschreibt, wie.

43.7.4.1 Beispiel: Prüfen, ob ein regulärer Ausdruck an einem bestimmten Index übereinstimmt

Da .test() von /y und .lastIndex beeinflusst wird, können wir es verwenden, um zu prüfen, ob ein regulärer Ausdruck regExp mit einem String str an einem bestimmten index übereinstimmt

function matchesStringAt(regExp, str, index) {
  if (!regExp.sticky) {
    throw new Error('Flag /y of regExp must be set');
  }
  regExp.lastIndex = index;
  return regExp.test(str);
}
assert.equal(
  matchesStringAt(/x+/y, 'aaxxx', 0), false);
assert.equal(
  matchesStringAt(/x+/y, 'aaxxx', 2), true);

regExp ist aufgrund von /y an .lastIndex gebunden.

Beachten Sie, dass wir die Assertion ^ nicht verwenden dürfen, die regExp an den Anfang des Eingabe-Strings binden würde.

43.7.4.2 Beispiel: Den Speicherort einer Übereinstimmung finden, beginnend an einem bestimmten Index

.search() ermöglicht es uns, den Speicherort zu finden, an dem ein regulärer Ausdruck übereinstimmt

> '#--#'.search(/#/)
0

Leider können wir nicht ändern, wo .search() nach Übereinstimmungen sucht. Als Workaround können wir .exec() für die Suche verwenden

function searchAt(regExp, str, index) {
  if (!regExp.global && !regExp.sticky) {
    throw new Error('Either flag /g or flag /y of regExp must be set');
  }
  regExp.lastIndex = index;
  const match = regExp.exec(str);
  if (match) {
    return match.index;
  } else {
    return -1;
  }
}

assert.equal(
  searchAt(/#/g, '#--#', 0), 0);
assert.equal(
  searchAt(/#/g, '#--#', 1), 3);
43.7.4.3 Beispiel: Ersetzen eines Vorkommens an einem bestimmten Index

Wenn .replace() ohne /g und mit /y verwendet wird, macht es eine Ersetzung – wenn eine Übereinstimmung bei .lastIndex vorhanden ist

function replaceOnceAt(str, regExp, replacement, index) {
  if (!(regExp.sticky && !regExp.global)) {
    throw new Error('Flag /y must be set, flag /g must not be set');
  }
  regExp.lastIndex = index;
  return str.replace(regExp, replacement);
}
assert.equal(
  replaceOnceAt('aa aaaa a', /a+/y, 'X', 0), 'X aaaa a');
assert.equal(
  replaceOnceAt('aa aaaa a', /a+/y, 'X', 3), 'aa X a');
assert.equal(
  replaceOnceAt('aa aaaa a', /a+/y, 'X', 8), 'aa aaaa X');

43.7.5 Die Nachteile von .lastIndex

Die Eigenschaft .lastIndex des regulären Ausdrucks hat zwei erhebliche Nachteile

Auf der positiven Seite bietet .lastIndex auch zusätzliche nützliche Funktionalität: Wir können diktieren, wo die Übereinstimmung beginnen soll (für einige Operationen).

43.7.6 Zusammenfassung: .global (/g) und .sticky (/y)

Die folgenden beiden Methoden werden von /g und /y überhaupt nicht beeinflusst

Diese Tabelle erklärt, wie die verbleibenden Methoden, die mit regulären Ausdrücken zu tun haben, von diesen beiden Flags beeinflusst werden

/ /g /y /yg
r.exec(s) {i:0} {i:1} {i:1} {i:1}
.lI unch .lI upd .lI upd .lI upd
r.test(s) true true true true
.lI unch .lI upd .lI upd .lI upd
s.match(r) {i:0} ["#","#","#"] {i:1} ["#","#"]
.lI unch .lI reset .lI upd .lI reset
s.matchAll(r) TypeError [{i:1}, {i:3}] TypeError [{i:1}]
.lI unch .lI unch
s.replace(r, 'x') "x#-#" "xx-x" "#x-#" "xx-#"
.lI unch .lI reset .lI upd .lI reset
s.replaceAll(r, 'x') TypeError "xx-x" TypeError "xx-#"
.lI reset .lI reset

Variablen

const r = /#/; r.lastIndex = 1;
const s = '##-#';

Abkürzungen

  Das Node.js-Skript, das die vorherige Tabelle generiert hat

Die vorherige Tabelle wurde über ein Node.js-Skript generiert.

43.8 Techniken für die Arbeit mit regulären Ausdrücken

43.8.1 Willkürlichen Text für reguläre Ausdrücke escapen

Die folgende Funktion escaped willkürlichen Text, sodass er exakt übereinstimmt, wenn wir ihn in einen regulären Ausdruck einfügen

function escapeForRegExp(str) {
  return str.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&'); // (A)
}
assert.equal(escapeForRegExp('[yes?]'), String.raw`\[yes\?\]`);
assert.equal(escapeForRegExp('_g_'), String.raw`_g_`);

In Zeile A escapen wir alle Syntaxzeichen. Wir müssen selektiv sein, da das Flag /u des regulären Ausdrucks viele Escape-Sequenzen verbietet – z. B.: \a \: \-

escapeForRegExp() hat zwei Anwendungsfälle

.replace() erlaubt uns nur, Plain Text einmal zu ersetzen. Mit escapeForRegExp() können wir diese Einschränkung umgehen

const plainText = ':-)';
const regExp = new RegExp(escapeForRegExp(plainText), 'ug');
assert.equal(
  ':-) :-) :-)'.replace(regExp, '🙂'), '🙂 🙂 🙂');

43.8.2 Alles oder nichts matchen

Manchmal benötigen wir einen regulären Ausdruck, der alles oder nichts matcht – zum Beispiel als Standardwert.