JavaScript for impatient programmers (ES2022版)
本書をサポートしてください:購入する または 寄付する
(広告です。ブロックしないでください。)

43章 正規表現(RegExp



  機能の可用性

特に明記されていない限り、すべての正規表現機能はES3以降で使用可能です。

43.1章 正規表現の作成

43.1.1項 リテラル vs. コンストラクタ

正規表現を作成する主な方法は2つあります。

どちらの正規表現も、次の2つの部分を持ちます。

43.1.2項 正規表現の複製と非破壊的な変更

コンストラクタRegExp()には2つのバリエーションがあります。

2番目のバリエーションは、正規表現を複製する際に、オプションで変更する場合に役立ちます。フラグは不変であり、これはフラグを変更する唯一の方法です。例:

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章 構文

43.2.1項 構文文字

正規表現の最上位レベルでは、次の構文文字は特殊です。これらは、バックスラッシュ(\)をプレフィックスとして付けることでエスケープされます。

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

正規表現リテラルでは、スラッシュをエスケープする必要があります。

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

new RegExp()の引数では、スラッシュをエスケープする必要はありません。

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

43.2.2項 基本アトム

アトムは、正規表現の基本的な構成要素です。

43.2.3項 Unicodeプロパティエスケープ [ES2018]

43.2.3.1項 Unicode文字プロパティ

Unicode標準では、各文字にプロパティ(メタデータ)があります。プロパティは、文字の性質を定義する上で重要な役割を果たします。Unicode標準、3.3節、D3を引用すると、

文字の意味論は、その同一性、規範的なプロパティ、および動作によって決定されます。

いくつかのプロパティの例を以下に示します。

43.2.3.2項 Unicodeプロパティエスケープ

Unicodeプロパティエスケープは次のようになります。

  1. \p{prop=value}:プロパティpropの値がvalueであるすべての文字に一致します。
  2. \P{prop=value}:プロパティpropの値がvalueでないすべての文字に一致します。
  3. \p{bin_prop}:バイナリプロパティbin_propがTrueであるすべての文字に一致します。
  4. \P{bin_prop}:バイナリプロパティbin_propがFalseであるすべての文字に一致します。

コメント

参考資料

43.2.4項 文字クラス

文字クラスは、角括弧内にクラス範囲を囲みます。クラス範囲は文字の集合を指定します。

クラス範囲のルール

43.2.5 グループ

43.2.6 量指定子

デフォルトでは、以下のすべての量指定子は貪欲です(可能な限り多くの文字にマッチします)

控えめ(可能な限り少ない文字にマッチ)にするには、後に疑問符(?)を付けます

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

43.2.7 アサーション

43.2.7.1 先読みアサーション

肯定先読み: (?=«パターン»)は、パターンが次に来るものにマッチする場合にマッチします。

例:小文字の文字列で、その後にXが続くもの。

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

X自体はマッチした部分文字列の一部ではありません。

否定先読み: (?!«パターン»)は、パターンが次に来るものにマッチしない場合にマッチします。

例:小文字の文字列で、その後にXが続かないもの。

> 'abcX def'.match(/[a-z]+(?!X)/g)
[ 'ab', 'def' ]
43.2.7.2 先読みアサーション [ES2018]

肯定後読み: (?<=«パターン»)は、パターンが前に来たものにマッチする場合にマッチします。

例:小文字の文字列で、その前にXが続くもの。

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

否定後読み: (?<!«パターン»)は、パターンが前に来たものにマッチしない場合にマッチします。

例:小文字の文字列で、その前にXが続かないもの。

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

例:「.js」を「.html」に置き換えますが、「Node.js」の場合は置き換えません。

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

43.2.8 分岐 (|)

注意点: この演算子の優先順位は低いです。必要に応じてグループを使用してください

43.3 フラグ

表21: これらは、JavaScriptでサポートされている正規表現フラグです。
リテラルフラグ プロパティ名 ES 説明
d hasIndices ES2022 マッチインデックスを有効にする
g global ES3 複数回マッチする
i ignoreCase ES3 大文字小文字を区別せずにマッチする
m multiline ES3 ^$が行単位でマッチする
s dotAll ES2018 ドットが改行文字にマッチする
u unicode ES6 Unicodeモード(推奨)
y sticky ES6 マッチ間の文字がない

JavaScriptでは以下の正規表現フラグが使用できます(表 21 は簡潔な概要を示しています)

43.3.1 正規表現フラグの順序

次の正規表現を考えてみます: /“([^”]+)”/udg

フラグはどのような順序でリストする必要がありますか?2つの選択肢があります

  1. アルファベット順: /dgu
  2. 重要度の順序(おそらく、/uが最も基本的ななど):/ugd

(2)は自明ではないため、(1)の方が良い選択肢です。JavaScriptもRegExpプロパティ.flagsでこれを使用しています

> /a/ismudgy.flags
'dgimsuy'

43.3.2 フラグ: /uによるUnicodeモード

フラグ/uは、正規表現の特別なUnicodeモードを有効にします。そのモードでは、いくつかの機能が有効になります

次の小節では、最後の項目を詳しく説明します。原子単位がUnicode文字である場合とJavaScript文字である場合を説明するために、次のUnicode文字を使用します

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

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

JavaScriptがどのように処理するかを示すために、🙂\uD83D\uDE42を切り替えるだけです。どちらも同等であり、文字列と正規表現で交換して使用できます。

43.3.2.1 結果: Unicode文字を文字クラスに配置できます

/uを使用すると、🙂の2つのコードユニットは1つの文字として扱われます

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

/uを使用しないと、🙂は2つの文字として扱われます

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

^$は、入力文字列が1文字であることを要求するため、最初の結果はfalseになります。

43.3.2.2 結果: ドット演算子(.)は、JavaScript文字ではなく、Unicode文字にマッチします

/uを使用すると、ドット演算子はUnicode文字にマッチします

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

.match()/gは、正規表現のすべてのマッチを含む配列を返します。

/uを使用しないと、ドット演算子はJavaScript文字にマッチします

> '\uD83D\uDE80'.match(/./g).length
2
43.3.2.3 結果: 量指定子は、JavaScript文字ではなく、Unicode文字に適用されます

/uを使用すると、量指定子は先行するUnicode文字全体に適用されます

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

/uを使用しないと、量指定子は先行するJavaScript文字のみに適用されます

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

43.4 正規表現オブジェクトのプロパティ

注目すべき点

43.4.1 フラグをプロパティとして

各正規表現フラグは、より長く、より記述的な名前を持つプロパティとして存在します

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

これはフラグプロパティの完全なリストです

43.4.2 その他のプロパティ

各正規表現には、次のプロパティもあります

43.5 マッチオブジェクト

いくつかの正規表現関連メソッドは、正規表現が入力文字列のどこでマッチしたかについての詳細情報を提供するために、いわゆるマッチオブジェクトを返します。これらのメソッドは次のとおりです。

これは例です

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

.exec()の結果は、次のプロパティを持つ最初のマッチのマッチオブジェクトです

43.5.1 マッチオブジェクトにおけるマッチインデックス [ES2022]

マッチインデックスはマッチオブジェクトの機能です。正規表現フラグ/d(プロパティ.hasIndices)をオンにすると、グループがキャプチャされた開始インデックスと終了インデックスが記録されます。

43.5.1.1 番号付きグループのマッチインデックス

番号付きグループのキャプチャへのアクセス方法を示します

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

正規表現フラグ/dにより、matchObjには、入力文字列で番号付きグループがキャプチャされた場所を記録するプロパティ.indicesもあります

assert.deepEqual(
  matchObj.indices[1], [0, 4]
);
assert.deepEqual(
  matchObj.indices[2], [4, 6]
);
43.5.1.2 名前付きグループのマッチインデックス

名前付きグループのキャプチャは、このようにアクセスされます

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

それらのインデックスはmatchObj.indices.groupsに格納されます

assert.deepEqual(
  matchObj.indices.groups.as, [0, 4]);
assert.deepEqual(
  matchObj.indices.groups.bs, [4, 6]);
43.5.1.3 より現実的な例

マッチインデックスの重要なユースケースの1つは、構文エラーが正確にどこにあるのかを示すパーサーです。次のコードは関連する問題を解決します。引用されたコンテンツがどこで始まり、どこで終わるのかを示します(デモは最後を参照)。

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 正規表現の操作方法

43.6.1 デフォルトでは、正規表現は文字列内の任意の場所にマッチします

デフォルトでは、正規表現は文字列内の任意の場所にマッチします

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

これは、^などのアサーションを使用するか、/yフラグを使用することで変更できます。

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

43.6.2 regExp.test(str):マッチがありますか?[ES3]

正規表現メソッド.test()は、regExpstrにマッチする場合はtrueを返します。

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

.test()では、通常/gフラグを避けるべきです。使用した場合、メソッドを呼び出すたびに同じ結果が得られるとは限りません。

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

この結果は、/a/が文字列内に2つのマッチを持つことによるものです。それらすべてが見つかった後、.test()falseを返します。

43.6.3 str.search(regExp):マッチはどのインデックスにありますか?[ES3]

文字列メソッド.search()は、regExpにマッチするstrの最初のインデックスを返します。

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

43.6.4 regExp.exec(str):キャプチャグループ[ES3]

43.6.4.1 最初のマッチに対するマッチオブジェクトの取得

/gフラグがない場合、.exec()str内のregExpの最初のマッチに対するマッチオブジェクトを返します。

assert.deepEqual(
  /(a+)b/.exec('ab aab'),
  {
    0: 'ab',
    1: 'a',
    index: 0,
    input: 'ab aab',
    groups: undefined,
  }
);
43.6.4.2 名前付きキャプチャグループ[ES2018]

前の例には、番号付きグループが1つ含まれていました。次の例は、名前付きグループを示しています。

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

.exec()の結果からわかるように、名前付きグループは番号付きグループでもあります。そのキャプチャは2回存在します。

43.6.4.3 すべてのマッチをループ処理する

  すべてのマッチを取得するためのより良い代替手段:str.matchAll(regExp) [ES2020]

ECMAScript 2020以降、JavaScriptにはすべてのマッチを取得するための別のメソッドがあります。str.matchAll(regExp)。このメソッドは使いやすく、注意点も少なくなっています。

正規表現のすべてのマッチ(最初の1つだけではない)を取得する場合は、/gフラグをオンにする必要があります。その後、.exec()を複数回呼び出して、毎回1つのマッチを取得できます。最後のマッチの後、.exec()nullを返します。

> 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

したがって、すべてのマッチを次のようにループ処理できます。

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'

  /g付きの正規表現を共有する際には注意してください!

/g付きの正規表現を共有することにはいくつかの落とし穴があり、それについては後で説明します

  練習問題:.exec()を使用した引用符付きテキストの抽出

exercises/regexps/extract_quoted_test.mjs

43.6.5 str.match(regExp):すべてのグループ0キャプチャを取得する[ES3]

/gがない場合、.match().exec()のように動作し、単一のマッチオブジェクトを返します。

/gがある場合、.match()regExpにマッチするstrのすべての部分文字列を返します。

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

マッチがない場合、.match()nullを返します。

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

null合体演算子(??を使用して、nullから身を守ることができます。

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

43.6.6 str.matchAll(regExp):すべてのマッチオブジェクトに対する反復可能オブジェクトを取得する[ES2020]

.matchAll()は次のように呼び出されます。

const matchIterable = str.matchAll(regExp);

文字列と正規表現が与えられると、.matchAll()はすべてのマッチのマッチオブジェクトに対する反復可能オブジェクトを返します。

次の例では、Array.from()を使用して反復可能オブジェクトを配列に変換し、より適切に比較できるようにしています。

> 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 },
]

/gフラグを設定する必要があります。

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

.matchAll()regExp.lastIndexの影響を受けず、変更もしません。

43.6.6.1 .matchAll()の実装

.matchAll()は次のように.exec()を使用して実装できます。

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

ローカルコピーを作成することで、2つのことが保証されます。

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()str.match()str.matchAll()

次の表は、3つのメソッドの違いをまとめたものです。

/gなし /gあり
regExp.exec(str) 最初のマッチオブジェクト 次のマッチオブジェクトまたはnull
str.match(regExp) 最初のマッチオブジェクト グループ0キャプチャの配列
str.matchAll(regExp) TypeError マッチオブジェクトに対する反復可能オブジェクト

43.6.8 str.replace()str.replaceAll()による置換

両方の置換メソッドには、2つのパラメーターがあります。

searchValueは次のいずれかです。

replacementValueは次のいずれかです。

2つのメソッドの違いは次のとおりです。

この表は、その動作をまとめたものです。

検索対象: 文字列 /gなしの正規表現 /gのある正規表現
.replace 最初の出現箇所 最初の出現箇所 (すべての出現箇所)
.replaceAll すべての出現箇所 TypeError すべての出現箇所

.replace()の最後の列は括弧で囲まれています。これは、このメソッドが.replaceAll()よりもずっと前に存在し、現在は後者のメソッドで処理する必要がある機能をサポートしているためです。これを変更できれば、.replace()はここでTypeErrorをスローします。

まず、replacementValueが単純な文字列(文字$なし)の場合、.replace().replaceAll()が個別にどのように動作するかを調べます。次に、より複雑な置換値によって両方がどのように影響を受けるかを調べます。

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

.replace()の動作は、その最初のパラメーターsearchValueの影響を受けます。

文字列のすべての出現箇所を置換する場合は、2つの選択肢があります。

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

.replaceAll()の動作は、その最初のパラメーターsearchValueの影響を受けます。

43.6.8.3 .replace().replaceAll()のパラメーターreplacementValue

これまで、パラメーターreplacementValueは単純な文字列でのみ使用してきましたが、もっと多くのことができます。その値が

43.6.8.4 replacementValueは文字列です

置換値が文字列の場合、ドル記号には特別な意味があります。正規表現によってマッチしたテキストを挿入します。

テキスト 結果
$$ 単一の$
$& 完全一致
$` マッチ前のテキスト
$' マッチ後のテキスト
$n 番号付きグループnのキャプチャ(n > 0)
$<name> 名前付きグループnameのキャプチャ[ES2018]

例:マッチした部分文字列の前、中、後にテキストを挿入します。

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

例:番号付きグループのキャプチャを挿入します。

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

例:名前付きグループのキャプチャを挿入します。

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

  練習問題:.replace()と名前付きグループを使用した引用符の変更

exercises/regexps/change_quotes_test.mjs

43.6.8.5 replacementValueは関数です

置換値が関数の場合、各置換を計算できます。次の例では、見つかった非負の整数を2倍にします。

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

置換関数は、次のパラメーターを取得します。マッチオブジェクトとよく似ていることに注意してください。これらのパラメーターはすべて位置指定ですが、名前を付ける方法も示しました。

groupsだけに関心がある場合は、次のテクニックを使用できます。

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

A行のrestパラメーターにより、argsにはすべてのパラメーターを含む配列が含まれています。B行の配列メソッド.at()を使用して、最後のパラメーターにアクセスします。

43.6.9 正規表現の操作に関するその他のメソッド

String.prototype.split()については、文字列に関する章で説明されていますString.prototype.split()の最初のパラメーターは、文字列または正規表現のいずれかです。後者の場合、グループのキャプチャが結果に表示されます。

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

43.7 /g/yフラグ、および.lastIndexプロパティ(高度な内容)

このセクションでは、RegExpフラグ/g/yがどのように機能し、RegExpプロパティ.lastIndexにどのように依存するかを調べます。また、驚くかもしれない.lastIndexの興味深いユースケースについても発見します。

43.7.1 /g/yフラグ

すべてのメソッドは/g/yに異なる反応を示します。これは、大まかな概要を示しています。

正規表現に/gフラグも/yフラグもない場合、マッチングは1回発生し、先頭から始まります。

/gまたは/yのいずれかがある場合、マッチングは入力文字列内の「現在の位置」を基準にして実行されます。その位置は、正規表現プロパティ.lastIndexに格納されます。

正規表現関連のメソッドには3つのグループがあります。

  1. 文字列メソッド.search(regExp).split(regExp)は、/g/y(したがって.lastIndexも)を完全に無視します。

  2. RegExpメソッド.exec(str).test(str)は、/gまたは/yのいずれかが設定されている場合、2つの点で変化します。

    まず、メソッドを繰り返し呼び出すことで、複数のマッチを取得します。毎回、別の結果(マッチオブジェクトまたはtrue)または「結果の終わり」値(nullまたはfalse)を返します。

    次に、正規表現プロパティ.lastIndexを使用して入力文字列をステップ実行します。一方では、.lastIndexはマッチングが開始される場所を決定します。

    • /gは、マッチは.lastIndex以降で始まる必要があることを意味します。

    • /yは、マッチは.lastIndexで始まる必要があることを意味します。つまり、正規表現の先頭は.lastIndexに固定されます。

      ^$は、通常どおりに動作し続けます。.multilineが設定されていない限り、入力文字列の先頭または末尾にマッチを固定します。設定されている場合、行の先頭または末尾に固定します。

    一方、.lastIndexは、前のマッチの最後のインデックスに1を加えた値に設定されます。

  3. その他のすべてのメソッドへの影響は以下のとおりです。

    • /g は複数のマッチにつながります。
    • /y は、.lastIndexから始まる1つのマッチにつながります。
    • /yg は、ギャップのない複数のマッチにつながります。

これは概要でした。以降のセクションでは、より詳細な説明を行います。

43.7.2 /g/yによってメソッドがどのように影響を受けるか

43.7.2.1 regExp.exec(str) [ES3]

/g/yがない場合、.exec().lastIndexを無視し、常に最初のマッチに対するマッチオブジェクトを返します。

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

/gがある場合、マッチは.lastIndex以降から始まる必要があります。.lastIndexは更新されます。マッチがない場合はnullが返されます。

> 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]

/yがある場合、マッチは正確に.lastIndexから始まる必要があります。.lastIndexは更新されます。マッチがない場合はnullが返されます。

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

/ygがある場合、.exec()/yの場合と同じ動作をします。

43.7.2.2 regExp.test(str) [ES3]

このメソッドは.exec()と同じ動作をしますが、マッチオブジェクトを返す代わりにtrueを返し、nullを返す代わりにfalseを返します。

例えば、/g/yもない場合、結果は常にtrueになります。

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

/gがある場合、2つのマッチがあります。

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

/yがある場合、1つのマッチしかありません。

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

/ygがある場合、.test()/yの場合と同じ動作をします。

43.7.2.3 str.match(regExp) [ES3]

/gがない場合、.match().exec()のように動作します。/yがない場合、または

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

/yがある場合

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

/gがある場合、すべてのマッチ(グループ0)が配列で返されます。.lastIndexは無視され、0にリセットされます。

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

/yg/gのように動作しますが、マッチ間のギャップはありません。

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

/gが設定されていない場合、.matchAll()は例外をスローします。

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

/gが設定されている場合、マッチングは.lastIndexから始まり、そのプロパティは変更されません。

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

/ygが設定されている場合、動作は/gの場合と同じですが、マッチ間のギャップはありません。

> 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]

/g/yがない場合、最初の出現箇所だけが置換されます。

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

/gがある場合、すべての出現箇所が置換されます。.lastIndexは無視されますが、0にリセットされます。

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

/yがある場合、.lastIndexにある(最初の)出現箇所だけが置換されます。.lastIndexは更新されます。

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

/yg/gのように動作しますが、マッチ間のギャップは許されません。

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

.replaceAll().replace()のように動作しますが、/gが設定されていない場合は例外をスローします。

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

43.7.3 /g/yの4つの落とし穴とその対処法

まず、/g/yの4つの落とし穴、そしてそれらの落とし穴に対処する方法を見ていきます。

43.7.3.1 落とし穴1: /gまたは/y付きの正規表現をインライン化できません

/g付きの正規表現はインライン化できません。例えば、以下のwhileループでは、条件がチェックされるたびに正規表現が新しく作成されます。そのため、その.lastIndexは常に0になり、ループは終了しません。

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

/yの場合も、問題は同じです。

43.7.3.2 落とし穴2: /gまたは/yを削除するとコードが壊れる可能性があります

コードが/g付きの正規表現を期待しており、.exec()または.test()の結果をループしている場合、/gのない正規表現は無限ループを引き起こす可能性があります。

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

無限ループになるのはなぜでしょうか?それは、.exec()が常に最初の結果であるマッチオブジェクトを返し、nullを返さないためです。

/yの場合も、問題は同じです。

43.7.3.3 落とし穴3: /gまたは/yを追加するとコードが壊れる可能性があります

.test()にはもう1つの注意点があります。それは.lastIndexの影響を受けるということです。したがって、正規表現が文字列に一致するかどうかを正確に1回だけチェックしたい場合、正規表現には/gを含めるべきではありません。そうでなければ、.test()を呼び出すたびに一般的に異なる結果が得られます。

> 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 ]

最初の呼び出しはマッチを行い.lastIndexを更新します。2回目の呼び出しはマッチを見つけず、.lastIndexを0にリセットします。

.test()専用に正規表現を作成する場合、おそらく/gは追加しません。しかし、置換とテストの両方で同じ正規表現を使用する場合、/gに遭遇する可能性は高まります。

繰り返しますが、この問題は/yにも存在します。

> const regExp = /^X/y;
> regExp.test('Xa')
true
> regExp.test('Xa')
false
> regExp.test('Xa')
true
43.7.3.4 落とし穴4: .lastIndexが0でない場合、コードは予期しない結果を生成する可能性があります

.lastIndexの影響を受けるすべての正規表現操作を考えると、多くのアルゴリズムにおいて、.lastIndexが最初に0であることを注意深く確認する必要があります。そうでなければ、予期しない結果になる可能性があります。

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

通常、新しく作成された正規表現の.lastIndexは0であり、例のように明示的に変更することはありません。しかし、正規表現を複数回使用すると、.lastIndexが0でないままになる可能性があります。

43.7.3.5 /g/yの落とし穴を回避する方法

/g.lastIndexの対処方法の例として、前の例のcountMatches()を再訪します。間違った正規表現がコードを壊すのを防ぐにはどうすればよいでしょうか?3つのアプローチを見てみましょう。

43.7.3.5.1 例外をスローする

まず、/gが設定されていないか、.lastIndexが0でない場合に例外をスローできます。

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 正規表現をクローンする

次に、パラメータをクローンできます。これには、regExpが変更されないという追加の利点があります。

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 .lastIndexまたはフラグの影響を受けない操作を使用する

いくつかの正規表現操作は、.lastIndexまたはフラグの影響を受けません。例えば、.match()/gが存在する場合、.lastIndexを無視します。

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!

ここでは、.lastIndexをチェックまたは修正していなくても、countMatches()は動作します。

43.7.4 .lastIndexのユースケース:特定のインデックスからマッチングを開始する

状態を保存する以外にも、.lastIndexは特定のインデックスからマッチングを開始するためにも使用できます。このセクションでは、その方法について説明します。

43.7.4.1 例:特定のインデックスで正規表現が一致するかどうかをチェックする

.test()/y.lastIndexの影響を受けるため、特定のindexで正規表現regExpが文字列strと一致するかどうかをチェックするために使用できます。

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/yのために.lastIndexに固定されています。

入力文字列の先頭にregExpを固定するアサーション^を使用してはならないことに注意してください。

43.7.4.2 例:特定のインデックスから始まるマッチの位置を見つける

.search()を使用すると、正規表現が一致する場所を見つけることができます。

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

しかし、.search()がマッチの検索を開始する場所を変更することはできません。回避策として、検索には.exec()を使用できます。

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 例:特定のインデックスにある出現箇所を置換する

/gなしで/yを使用して使用する場合、.replace()は1つの置換を行います(.lastIndexにマッチがある場合)。

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 .lastIndexの欠点

正規表現のプロパティ.lastIndexには、2つの重要な欠点があります。

良い面としては、.lastIndexは追加の便利な機能も提供します。マッチングを開始する場所を指定できます(一部の操作の場合)。

43.7.6 まとめ:.global/g)と.sticky/y

次の2つのメソッドは、/g/yの両方から完全に影響を受けません。

この表は、残りの正規表現関連メソッドがこれら2つのフラグによってどのように影響を受けるかを示しています。

/ /g /y /yg
r.exec(s) {i:0} {i:1} {i:1} {i:1}
.lI変更なし .lI更新 .lI更新 .lI更新
r.test(s) true true true true
.lI変更なし .lI更新 .lI更新 .lI更新
s.match(r) {i:0} ["#","#","#"] {i:1} ["#","#"]
.lI変更なし .lIリセット .lI更新 .lIリセット
s.matchAll(r) TypeError [{i:1}, {i:3}] TypeError [{i:1}]
.lI変更なし .lI変更なし
s.replace(r, 'x') "x#-#" "xx-x" "#x-#" "xx-#"
.lI変更なし .lIリセット .lI更新 .lIリセット
s.replaceAll(r, 'x') TypeError "xx-x" TypeError "xx-#"
.lIリセット .lIリセット

変数

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

略語

  前の表を生成したNode.jsスクリプト

前の表は、Node.jsスクリプトによって生成されました。

43.8 正規表現を扱うためのテクニック

43.8.1 正規表現のための任意のテキストのエスケープ

次の関数は、任意のテキストをエスケープします。これにより、正規表現内に配置した場合、テキストがそのまま一致するようになります。

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

A行では、すべての構文文字をエスケープします。正規表現フラグ/uは多くのエスケープ(例:\a \: \-)を禁止するため、選択的に行う必要があります。

escapeForRegExp()には2つのユースケースがあります。

.replace()では、プレーンテキストを一度だけ置換できます。escapeForRegExp()を使用すると、この制限を回避できます。

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

43.8.2 すべてまたは何もマッチしない

場合によっては、すべてまたは何もマッチしない正規表現が必要になる場合があります(デフォルト値など)。