23. 正規表現の新機能
目次
この本をサポートしてください: 購入する (PDF, EPUB, MOBI) または 寄付する
(広告です。ブロックしないでください。)

23. 正規表現の新機能

この章では、ECMAScript 6における正規表現の新機能について説明します。ES5の正規表現機能とUnicodeに精通していると理解しやすいでしょう。必要に応じて、「Speaking JavaScript」の以下の2つの章を参照してください。



23.1 概要

ECMAScript 6では、以下の正規表現機能が新しく追加されました。

23.2 新しいフラグ /y (スティッキー)

新しいフラグ /y は、文字列に対して正規表現 re を照合する際に、2つの点を変更します。

このマッチング動作の主なユースケースは、各一致が直前の後続に続くようにするトークン化です。スティッキー正規表現と exec() によるトークン化の例は後ほど示します。

さまざまな正規表現操作が /y フラグにどのように反応するかを見てみましょう。次の表は概要を示しています。詳細は後ほど説明します。

正規表現のメソッド (re はメソッドが呼び出される正規表現です)

  フラグ マッチング開始 固定対象 一致した場合の結果 一致しない場合 re.lastIndex
exec() 0 マッチオブジェクト null 変更なし
  /g re.lastIndex マッチオブジェクト null 一致後のインデックス
  /y re.lastIndex re.lastIndex マッチオブジェクト null 一致後のインデックス
  /gy re.lastIndex re.lastIndex マッチオブジェクト null 一致後のインデックス
test() (任意) (exec()と同様) (exec()と同様) true false (exec()と同様)

文字列のメソッド (str はメソッドが呼び出される文字列、r は正規表現パラメータです)

  フラグ マッチング開始 固定対象 一致した場合の結果 一致しない場合 r.lastIndex
search() –, /g 0 一致のインデックス -1 変更なし
  /y, /gy 0 0 一致のインデックス -1 変更なし
match() 0 マッチオブジェクト null 変更なし
  /y r.lastIndex r.lastIndex マッチオブジェクト null 一致後
            のインデックス
  /g 前の後 一致を含む配列 null 0
    一致 (ループ)        
  /gy 前の後 前の後 一致を含む配列 null 0
    一致 (ループ) のインデックス      
split() –, /g 前の後 文字列の配列 [str] 変更なし
    一致 (ループ)   一致間    
  /y, /gy 前の後 前の後 空文字列を含む配列 [str] 変更なし
    一致 (ループ) のインデックス 一致間    
replace() 0 最初の一致を置換 置換なし 変更なし
  /y 0 0 最初の一致を置換 置換なし 変更なし
  /g 前の後 すべての一致を置換 置換なし 変更なし
    一致 (ループ)        
  /gy 前の後 前の後 すべての一致を置換 置換なし 変更なし
    一致 (ループ) のインデックス      

23.2.1 RegExp.prototype.exec(str)

/g が設定されていない場合、マッチングは常に先頭から開始されますが、一致が見つかるまでスキップします。 REGEX.lastIndex は変更されません。

const REGEX = /a/;

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

/g が設定されている場合、マッチングは REGEX.lastIndex から開始され、一致が見つかるまでスキップします。 REGEX.lastIndex は一致後の位置に設定されます。つまり、exec()null を返すまでループすると、すべての一致を受け取ります。-

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

/y のみが設定されている場合、マッチングは REGEX.lastIndex から開始され、その位置に固定されます (一致が見つかるまでスキップしません)。 REGEX.lastIndex/g が設定されている場合と同様に更新されます。

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

/y/g の両方を設定することは、/y のみを設定することと同じです。

23.2.2 RegExp.prototype.test(str)

test()exec() と同じように動作しますが、マッチングが成功または失敗したときに true または false を返します (マッチオブジェクトまたは null ではなく)。

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() はフラグ /glastIndex を無視します (これも変更されません)。文字列の先頭から開始して、最初の一致を探し、そのインデックスを返します (一致がない場合は -1 を返します)。

const REGEX = /a/;

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

フラグ /y を設定した場合、lastIndex は依然として無視されますが、正規表現はインデックス0に固定されるようになります。

const REGEX = /a/y;

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

23.2.4 String.prototype.match(regex)

match() には2つのモードがあります。

フラグ /g が設定されていない場合、match()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
}

フラグ /g のみが設定されている場合、match() はすべての一致する部分文字列を配列 (または null) で返します。マッチングは常に位置0から開始されます。-

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

さらにフラグ /y を設定すると、マッチングは繰り返し実行されますが、正規表現は前回の一致 (または0) 後のインデックスに固定されます。

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)

split() の詳細は Speaking JavaScript で説明されています

ES6では、フラグ /y を使用した場合にどのように変化するかを確認することが重要です。

/y を使用すると、文字列は区切り文字で始まる必要があります。

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

後続の区切り文字は、最初の区切り文字の直後に続く場合にのみ認識されます。

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

つまり、最初の区切り文字の前の文字列と区切り文字の間の文字列は常に空になります。

通常どおり、グループを使用して区切り文字の一部を結果配列に入れることができます。

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

23.2.6 String.prototype.replace(search, replacement)

フラグ /g がない場合、replace() は最初の一致のみを置換します。

const REGEX = /a/;

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

/y のみが設定されている場合も、最大で1つの一致が得られますが、その一致は常に文字列の先頭に固定されます。 lastIndex は無視され、変更されません。

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'

/g が設定されている場合、replace() はすべての一致を置換します。

const REGEX = /a/g;

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

/gy が設定されている場合、replace() はすべての一致を置換しますが、各一致は前回の一致の末尾に固定されます。

const REGEX = /a/gy;

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

パラメータ replacement は関数にすることもできます。 詳細は「Speaking JavaScript」を参照してください

23.2.7 例: トークン化のためのスティッキーマッチングの使用

スティッキーマッチングの主なユースケースは、テキストをトークンのシーケンスに変換する*トークン化*です。トークン化の重要な特徴の1つは、トークンがテキストの断片であり、それらの間にギャップがあってはならないことです。したがって、スティッキーマッチングはここで最適です。

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;

トークンの有効なシーケンスでは、スティッキーマッチングとスティッキーでないマッチングは同じ出力を生成します。

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

ただし、文字列にトークン以外のテキストがある場合、スティッキーマッチングはトークン化を停止しますが、スティッキーでないマッチングはトークン以外のテキストをスキップします。

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

トークン化中のスティッキーマッチングの動作は、エラー処理に役立ちます。

23.2.8 例: スティッキーマッチングの手動実装

スティッキーマッチングを手動で実装する場合、次のようにします。関数 execSticky() は、スティッキーモードの RegExp.prototype.exec() と同様に動作します。

 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 新しいフラグ /u (Unicode)

フラグ /u は、正規表現の特別なUnicodeモードをオンにします。このモードには2つの機能があります。

  1. コードポイントを介して文字を指定するために、\u{1F42A} などのUnicodeコードポイントエスケープシーケンスを使用できます。 \u03B1 などの通常のUnicodeエスケープは、4桁の16進数 (基本多言語面に相当) の範囲しかありません。
  2. 正規表現パターンと文字列の「文字」はコードポイントです (UTF-16コードユニットではありません)。コードユニットはコードポイントに変換されます。

Unicodeに関する章のセクションには、エスケープシーケンスに関する詳細情報があります。次に、機能2の結果について説明します。Unicodeコードポイントエスケープ (例: \u{1F680}) の代わりに、2つのUTF-16コードユニット (例: \uD83D\uDE80}) を使用しています。これにより、サロゲートペアがUnicodeモードでグループ化され、Unicodeモードと非Unicodeモードの両方で機能することが明確になります。

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

23.3.1 結果: 正規表現内の単一のサロゲートは、単一のサロゲートとのみ一致する

非Unicodeモードでは、正規表現内の単一のサロゲートは、(サロゲートペアエンコーディング) コードポイント内でも見つかります。

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

Unicodeモードでは、サロゲートペアはアトミックユニットになり、単一のサロゲートは「内部」では見つかりません。

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

実際の単一のサロゲートはまだ見つかります。

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

23.3.2 結果:文字クラスにコードポイントを記述できる

Unicode モードでは、文字クラスにコードポイントを記述できます。そして、それらはもはや2文字として解釈されません。

> /^[\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 結果:ドット演算子(.)はコードユニットではなく、コードポイントにマッチする

Unicode モードでは、ドット演算子はコードポイント(1つまたは2つのコードユニット)にマッチします。非 Unicode モードでは、単一のコードユニットにマッチします。例えば

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

23.3.4 結果:量指定子はコードユニットではなく、コードポイントに適用される

Unicode モードでは、量指定子はコードポイント(1つまたは2つのコードユニット)に適用されます。非 Unicode モードでは、単一のコードユニットに適用されます。例えば

> /\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 新しいデータプロパティ flags

ECMAScript 6 では、正規表現は以下のデータプロパティを持ちます。

ちなみに、lastIndex は現在唯一のインスタンスプロパティです。他のすべてのデータプロパティは、get RegExp.prototype.global のような内部インスタンスプロパティとゲッターを介して実装されています。

プロパティ source(ES5ですでに存在していました)には、正規表現パターンが文字列として含まれています。

> /abc/ig.source
'abc'

プロパティ flags は新しく、フラグを文字列として含み、フラグごとに1文字が含まれています。

> /abc/ig.flags
'gi'

既存の正規表現のフラグを変更することはできません(ignoreCase などは常に不変です)。しかし、flags を使用すると、フラグが変更されたコピーを作成できます。

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

次のセクションでは、正規表現の変更されたコピーを作成する別の方法について説明します。

23.5 RegExp() はコピーコンストラクターとして使用できる

ES6 では、コンストラクター RegExp() には2つのバリアントがあります(2つ目は新しいものです)。

以下のインタラクションは後者のバリアントを示しています。

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

したがって、RegExp コンストラクターはフラグを変更する別の方法を提供します。

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

23.5.1 例: exec() の反復可能なバージョン

以下の関数 execAll() は、exec() の反復可能なバージョンであり、正規表現のすべての一致を取得するために exec() を使用することによるいくつかの問題を修正します。

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);
}

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 正規表現メソッドに委任する文字列メソッド

以下の文字列メソッドは、正規表現メソッドに作業の一部を委任するようになりました。

詳細については、文字列の章の「正規表現の作業をパラメータに委任する文字列メソッド」セクションを参照してください。

次: 24. 非同期プログラミング(バックグラウンド)