23. Neue Funktionen für reguläre Ausdrücke
Inhaltsverzeichnis
Bitte unterstützen Sie dieses Buch: kaufen Sie es (PDF, EPUB, MOBI) oder spenden Sie
(Werbung, bitte nicht blockieren.)

23. Neue Funktionen für reguläre Ausdrücke

Dieses Kapitel erklärt neue Funktionen für reguläre Ausdrücke in ECMAScript 6. Es ist hilfreich, wenn Sie mit den regulären Ausdrücken von ES5 und Unicode vertraut sind. Konsultieren Sie bei Bedarf die folgenden beiden Kapitel von „Speaking JavaScript“



23.1 Übersicht

Die folgenden Funktionen für reguläre Ausdrücke sind neu in ECMAScript 6

23.2 Neues Flag /y (sticky)

Das neue Flag /y ändert zwei Dinge bei der Übereinstimmung eines regulären Ausdrucks re mit einem String

Der Hauptanwendungsfall für dieses Matching-Verhalten ist das Tokenisieren, bei dem jede Übereinstimmung unmittelbar auf die vorhergehende folgen soll. Ein Beispiel für Tokenisieren über einen Sticky-Regulären-Ausdruck und exec() wird später gegeben.

Betrachten wir, wie sich verschiedene Operationen mit regulären Ausdrücken auf das /y-Flag auswirken. Die folgenden Tabellen geben einen Überblick. Im Folgenden werde ich weitere Details erläutern.

Methoden von regulären Ausdrücken (re ist der reguläre Ausdruck, auf den eine Methode aufgerufen wird)

  Flags Starten des Abgleichs Verankert an Ergebnis, wenn Übereinstimmung Keine Übereinstimmung re.lastIndex
exec() 0 Match-Objekt null unverändert
  /g re.lastIndex Match-Objekt null Index nach der Übereinstimmung
  /y re.lastIndex re.lastIndex Match-Objekt null Index nach der Übereinstimmung
  /gy re.lastIndex re.lastIndex Match-Objekt null Index nach der Übereinstimmung
test() (Beliebig) (wie exec()) (wie exec()) true false (wie exec())

Methoden von Strings (str ist der String, auf den eine Methode aufgerufen wird, r ist der Parameter des regulären Ausdrucks)

  Flags Starten des Abgleichs Verankert an Ergebnis, wenn Übereinstimmung Keine Übereinstimmung r.lastIndex
search() –, /g 0 Index der Übereinstimmung -1 unverändert
  /y, /gy 0 0 Index der Übereinstimmung -1 unverändert
match() 0 Match-Objekt null unverändert
  /y r.lastIndex r.lastIndex Match-Objekt null Index nach
            Übereinstimmung
  /g Nach vorheriger Array mit Übereinstimmungen null 0
    Übereinstimmung (Schleife)        
  /gy Nach vorheriger Nach vorheriger Array mit Übereinstimmungen null 0
    Übereinstimmung (Schleife) Übereinstimmung      
split() –, /g Nach vorheriger Array mit Strings [str] unverändert
    Übereinstimmung (Schleife)   zwischen Übereinstimmungen    
  /y, /gy Nach vorheriger Nach vorheriger Array mit leeren Strings [str] unverändert
    Übereinstimmung (Schleife) Übereinstimmung zwischen Übereinstimmungen    
replace() 0 Erste Übereinstimmung ersetzt Keine Ersetzung unverändert
  /y 0 0 Erste Übereinstimmung ersetzt Keine Ersetzung unverändert
  /g Nach vorheriger Alle Übereinstimmungen ersetzt Keine Ersetzung unverändert
    Übereinstimmung (Schleife)        
  /gy Nach vorheriger Nach vorheriger Alle Übereinstimmungen ersetzt Keine Ersetzung unverändert
    Übereinstimmung (Schleife) Übereinstimmung      

23.2.1 RegExp.prototype.exec(str)

Wenn /g nicht gesetzt ist, beginnt der Abgleich immer am Anfang, überspringt aber so lange, bis eine Übereinstimmung gefunden wird. REGEX.lastIndex wird nicht geändert.

const REGEX = /a/;

REGEX.lastIndex = 7; // ignored
const match = REGEX.exec('xaxa');
console.log(match.index); // 1
console.log(REGEX.lastIndex); // 7 (unchanged)

Wenn /g gesetzt ist, beginnt der Abgleich bei REGEX.lastIndex und überspringt, bis eine Übereinstimmung gefunden wird. REGEX.lastIndex wird auf die Position nach der Übereinstimmung gesetzt. Das bedeutet, dass Sie alle Übereinstimmungen erhalten, wenn Sie so lange schleifen, bis exec() null zurückgibt.

const REGEX = /a/g;

REGEX.lastIndex = 2;
const match = REGEX.exec('xaxa');
console.log(match.index); // 3
console.log(REGEX.lastIndex); // 4 (updated)

// No match at index 4 or later
console.log(REGEX.exec('xaxa')); // null

Wenn nur /y gesetzt ist, beginnt der Abgleich bei REGEX.lastIndex und ist an dieser Position verankert (kein Überspringen, bis eine Übereinstimmung gefunden wird). REGEX.lastIndex wird ähnlich wie bei gesetztem /g aktualisiert.

const REGEX = /a/y;

// No match at index 2
REGEX.lastIndex = 2;
console.log(REGEX.exec('xaxa')); // null

// Match at index 3
REGEX.lastIndex = 3;
const match = REGEX.exec('xaxa');
console.log(match.index); // 3
console.log(REGEX.lastIndex); // 4

Das Setzen von sowohl /y als auch /g ist dasselbe wie nur /y zu setzen.

23.2.2 RegExp.prototype.test(str)

test() funktioniert wie exec(), gibt aber true oder false zurück (anstelle eines Match-Objekts oder null), wenn der Abgleich erfolgreich ist oder fehlschlägt

const REGEX = /a/y;

REGEX.lastIndex = 2;
console.log(REGEX.test('xaxa')); // false

REGEX.lastIndex = 3;
console.log(REGEX.test('xaxa')); // true
console.log(REGEX.lastIndex); // 4

23.2.3 String.prototype.search(regex)

search() ignoriert das Flag /g und lastIndex (das weder geändert noch verwendet wird). Beginnend am Anfang des Strings sucht es nach der ersten Übereinstimmung und gibt deren Index zurück (oder -1, wenn keine Übereinstimmung gefunden wurde)

const REGEX = /a/;

REGEX.lastIndex = 2; // ignored
console.log('xaxa'.search(REGEX)); // 1

Wenn Sie das Flag /y setzen, wird lastIndex immer noch ignoriert, aber der reguläre Ausdruck ist jetzt bei Index 0 verankert.

const REGEX = /a/y;

REGEX.lastIndex = 1; // ignored
console.log('xaxa'.search(REGEX)); // -1 (no match)

23.2.4 String.prototype.match(regex)

match() hat zwei Modi

Wenn das Flag /g nicht gesetzt ist, erfasst match() Gruppen wie exec()

{
    const REGEX = /a/;

    REGEX.lastIndex = 7; // ignored
    console.log('xaxa'.match(REGEX).index); // 1
    console.log(REGEX.lastIndex); // 7 (unchanged)
}
{
    const REGEX = /a/y;

    REGEX.lastIndex = 2;
    console.log('xaxa'.match(REGEX)); // null

    REGEX.lastIndex = 3;
    console.log('xaxa'.match(REGEX).index); // 3
    console.log(REGEX.lastIndex); // 4
}

Wenn nur das Flag /g gesetzt ist, gibt match() alle übereinstimmenden Teilstrings in einem Array zurück (oder null). Der Abgleich beginnt immer bei Position 0.

const REGEX = /a|b/g;
REGEX.lastIndex = 7;
console.log('xaxb'.match(REGEX)); // ['a', 'b']
console.log(REGEX.lastIndex); // 0

Wenn Sie zusätzlich das Flag /y setzen, wird der Abgleich weiterhin wiederholt durchgeführt, wobei der reguläre Ausdruck am Index nach der vorherigen Übereinstimmung (oder 0) verankert wird.

const REGEX = /a|b/gy;

REGEX.lastIndex = 0; // ignored
console.log('xab'.match(REGEX)); // null
REGEX.lastIndex = 1; // ignored
console.log('xab'.match(REGEX)); // null

console.log('ab'.match(REGEX)); // ['a', 'b']
console.log('axb'.match(REGEX)); // ['a']

23.2.5 String.prototype.split(separator, limit)

Die vollständigen Details zu split() werden in Speaking JavaScript erklärt.

Für ES6 ist es interessant zu sehen, wie sich die Dinge ändern, wenn Sie das Flag /y verwenden.

Mit /y muss der String mit einem Trennzeichen beginnen

> 'x##'.split(/#/y) // no match
[ 'x##' ]
> '##x'.split(/#/y) // 2 matches
[ '', '', 'x' ]

Nachfolgende Trennzeichen werden nur erkannt, wenn sie unmittelbar auf das erste Trennzeichen folgen

> '#x#'.split(/#/y) // 1 match
[ '', 'x#' ]
> '##'.split(/#/y) // 2 matches
[ '', '', '' ]

Das bedeutet, dass der String vor dem ersten Trennzeichen und die Strings zwischen den Trennzeichen immer leer sind.

Wie üblich können Sie Gruppen verwenden, um Teile der Trennzeichen in das Ergebnis-Array aufzunehmen

> '##'.split(/(#)/y)
[ '', '#', '', '#', '' ]

23.2.6 String.prototype.replace(search, replacement)

Ohne das Flag /g ersetzt replace() nur die erste Übereinstimmung

const REGEX = /a/;

// One match
console.log('xaxa'.replace(REGEX, '-')); // 'x-xa'

Wenn nur /y gesetzt ist, erhalten Sie ebenfalls höchstens eine Übereinstimmung, aber diese ist immer am Anfang des Strings verankert. lastIndex wird ignoriert und unverändert gelassen.

const REGEX = /a/y;

// Anchored to beginning of string, no match
REGEX.lastIndex = 1; // ignored
console.log('xaxa'.replace(REGEX, '-')); // 'xaxa'
console.log(REGEX.lastIndex); // 1 (unchanged)

// One match
console.log('axa'.replace(REGEX, '-')); // '-xa'

Mit gesetztem /g ersetzt replace() alle Übereinstimmungen

const REGEX = /a/g;

// Multiple matches
console.log('xaxa'.replace(REGEX, '-')); // 'x-x-'

Mit gesetztem /gy ersetzt replace() alle Übereinstimmungen, aber jede Übereinstimmung ist am Ende der vorherigen Übereinstimmung verankert

const REGEX = /a/gy;

// Multiple matches
console.log('aaxa'.replace(REGEX, '-')); // '--xa'

Der Parameter replacement kann auch eine Funktion sein. Konsultieren Sie „Speaking JavaScript“ für Details.

23.2.7 Beispiel: Verwenden von Sticky-Matching zum Tokenisieren

Der Hauptanwendungsfall für Sticky-Matching ist das *Tokenisieren*, d. h. das Umwandeln eines Textes in eine Folge von Tokens. Ein wichtiges Merkmal des Tokenisierens ist, dass Tokens Fragmente des Textes sind und dass keine Lücken zwischen ihnen bestehen dürfen. Daher ist Sticky-Matching hier perfekt.

function tokenize(TOKEN_REGEX, str) {
    const result = [];
    let match;
    while (match = TOKEN_REGEX.exec(str)) {
        result.push(match[1]);
    }
    return result;
}

const TOKEN_GY = /\s*(\+|[0-9]+)\s*/gy;
const TOKEN_G  = /\s*(\+|[0-9]+)\s*/g;

In einer gültigen Token-Sequenz ergeben Sticky-Matching und Nicht-Sticky-Matching die gleiche Ausgabe

> tokenize(TOKEN_GY, '3 + 4')
[ '3', '+', '4' ]
> tokenize(TOKEN_G, '3 + 4')
[ '3', '+', '4' ]

Wenn jedoch nicht-tokenisierter Text im String vorhanden ist, stoppt Sticky-Matching das Tokenisieren, während Nicht-Sticky-Matching den nicht-tokenisierten Text überspringt

> tokenize(TOKEN_GY, '3x + 4')
[ '3' ]
> tokenize(TOKEN_G, '3x + 4')
[ '3', '+', '4' ]

Das Verhalten von Sticky-Matching während des Tokenisierens hilft bei der Fehlerbehandlung.

23.2.8 Beispiel: Manuellen Sticky-Matching implementieren

Wenn Sie Sticky-Matching manuell implementieren wollten, würden Sie es wie folgt tun: Die Funktion execSticky() funktioniert wie RegExp.prototype.exec() im Sticky-Modus.

 function execSticky(regex, str) {
     // Anchor the regex to the beginning of the string
     let matchSource = regex.source;
     if (!matchSource.startsWith('^')) {
         matchSource = '^' + matchSource;
     }
     // Ensure that instance property `lastIndex` is updated
     let matchFlags = regex.flags; // ES6 feature!
     if (!regex.global) {
         matchFlags = matchFlags + 'g';
     }
     const matchRegex = new RegExp(matchSource, matchFlags);

     // Ensure we start matching `str` at `regex.lastIndex`
     const matchOffset = regex.lastIndex;
     const matchStr = str.slice(matchOffset);
     let match = matchRegex.exec(matchStr);

     // Translate indices from `matchStr` to `str`
     regex.lastIndex = matchRegex.lastIndex + matchOffset;
     match.index = match.index + matchOffset;
     return match;
 }

23.3 Neues Flag /u (unicode)

Das Flag /u schaltet einen speziellen Unicode-Modus für einen regulären Ausdruck ein. Dieser Modus hat zwei Funktionen

  1. Sie können Unicode-Codepoint-Escape-Sequenzen wie \u{1F42A} verwenden, um Zeichen über Codepoints anzugeben. Normale Unicode-Escapes wie \u03B1 haben nur einen Bereich von vier Hexadezimalziffern (was der Basic Multilingual Plane entspricht).
  2. „Zeichen“ im Muster des regulären Ausdrucks und im String sind Codepoints (nicht UTF-16 Code-Units). Code-Units werden in Codepoints umgewandelt.

Ein Abschnitt im Kapitel über Unicode enthält weitere Informationen zu Escape-Sequenzen. Ich werde die Konsequenzen von Funktion 2 als Nächstes erläutern. Anstelle von Unicode-Codepoint-Escapes (z. B. \u{1F680}) verwende ich zwei UTF-16 Code-Units (z. B. \uD83D\uDE80). Das macht deutlich, dass Surrogatpaare im Unicode-Modus gruppiert werden und sowohl im Unicode-Modus als auch im Nicht-Unicode-Modus funktionieren.

> '\u{1F680}' === '\uD83D\uDE80' // code point vs. surrogate pairs
true

23.3.1 Konsequenz: alleinstehende Surrogates im regulären Ausdruck passen nur auf alleinstehende Surrogates

Im Nicht-Unicode-Modus wird ein alleinstehender Surrogate in einem regulären Ausdruck sogar innerhalb von (Surrogatpaare kodierenden) Codepoints gefunden

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

Im Unicode-Modus werden Surrogatpaare zu atomaren Einheiten und alleinstehende Surrogates werden „innerhalb“ von ihnen nicht gefunden

> /\uD83D/u.test('\uD83D\uDC2A')
false

Tatsächlich alleinstehende Surrogates werden immer noch gefunden

> /\uD83D/u.test('\uD83D \uD83D\uDC2A')
true
> /\uD83D/u.test('\uD83D\uDC2A \uD83D')
true

23.3.2 Konsequenz: Sie können Codepoints in Zeichenklassen einfügen

Im Unicode-Modus können Sie Codepoints in Zeichenklassen einfügen und sie werden nicht mehr als zwei Zeichen interpretiert.

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

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

23.3.3 Konsequenz: Der Punkt-Operator (.) passt auf Codepoints, nicht auf Code-Units

Im Unicode-Modus passt der Punkt-Operator auf Codepoints (ein oder zwei Code-Units). Im Nicht-Unicode-Modus passt er auf einzelne Code-Units. Zum Beispiel

> '\uD83D\uDE80'.match(/./gu).length
1
> '\uD83D\uDE80'.match(/./g).length
2

23.3.4 Konsequenz: Quantifizierer gelten für Codepoints, nicht für Code-Units

Im Unicode-Modus gelten Quantifizierer für Codepoints (ein oder zwei Code-Units). Im Nicht-Unicode-Modus gelten sie für einzelne Code-Units. Zum Beispiel

> /\uD83D\uDE80{2}/u.test('\uD83D\uDE80\uD83D\uDE80')
true

> /\uD83D\uDE80{2}/.test('\uD83D\uDE80\uD83D\uDE80')
false
> /\uD83D\uDE80{2}/.test('\uD83D\uDE80\uDE80')
true

23.4 Neue Dateneigenschaft flags

In ECMAScript 6 haben reguläre Ausdrücke die folgenden Dateneigenschaften

Nebenbei bemerkt, ist lastIndex die einzige Instanz-Eigenschaft, alle anderen Dateneigenschaften werden über interne Instanz-Eigenschaften und Getter wie get RegExp.prototype.global implementiert.

Die Eigenschaft source (die es bereits in ES5 gab) enthält das Muster des regulären Ausdrucks als String

> /abc/ig.source
'abc'

Die Eigenschaft flags ist neu, sie enthält die Flags als String, mit einem Zeichen pro Flag

> /abc/ig.flags
'gi'

Sie können die Flags eines vorhandenen regulären Ausdrucks nicht ändern (ignoreCase usw. waren schon immer unveränderlich), aber flags ermöglicht es Ihnen, eine Kopie zu erstellen, bei der die Flags geändert wurden

function copyWithIgnoreCase(re) {
    return new RegExp(re.source,
        re.flags.includes('i') ? re.flags : re.flags+'i');
}

Der nächste Abschnitt erklärt eine weitere Möglichkeit, modifizierte Kopien von regulären Ausdrücken zu erstellen.

23.5 RegExp() kann als Kopierkonstruktor verwendet werden

In ES6 gibt es zwei Varianten des Konstruktors RegExp() (die zweite ist neu)

Die folgende Interaktion demonstriert die letztere Variante

> new RegExp(/abc/ig).flags
'gi'
> new RegExp(/abc/ig, 'i').flags // change flags
'i'

Daher gibt uns der RegExp-Konstruktor eine weitere Möglichkeit, Flags zu ändern

function copyWithIgnoreCase(re) {
    return new RegExp(re,
        re.flags.includes('i') ? re.flags : re.flags+'i');
}

23.5.1 Beispiel: Eine iterierbare Version von exec()

Die folgende Funktion execAll() ist eine iterierbare Version von exec(), die mehrere Probleme mit der Verwendung von exec() zur Abfrage aller Übereinstimmungen eines regulären Ausdrucks behebt

function* execAll(regex, str) {
    // Make sure flag /g is set and regex.index isn’t changed
    const localCopy = copyAndEnsureFlag(regex, 'g');
    let match;
    while (match = localCopy.exec(str)) {
        yield match;
    }
}
function copyAndEnsureFlag(re, flag) {
    return new RegExp(re,
        re.flags.includes(flag) ? re.flags : re.flags+flag);
}

Verwendung von execAll()

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

// Access capture of group #1 via destructuring
for (const [, group1] of execAll(regex, str)) {
    console.log(group1);
}
// Output:
// fee
// fi
// fo
// fum

23.6 String-Methoden, die an reguläre Ausdrucksmethoden delegieren

Die folgenden String-Methoden delegieren nun einen Teil ihrer Arbeit an reguläre Ausdrucksmethoden

Weitere Informationen finden Sie in Abschn. „String-Methoden, die reguläre Ausdrucksarbeit an ihre Parameter delegieren“ im Kapitel über Strings.

Weiter: 24. Asynchrone Programmierung (Hintergrund)