8月21日、海外メディアSmashing Magazineにおいて、「Regexes Got Good: The History And Future Of Regular Expressions In JavaScript」と題した記事が公開されました。
JavaScriptの正規表現はかつて、他のプログラミング言語と比較して性能が劣るとされていましたが、近年の数多くの改良により、その評価は変わりつつあります。この記事では、JavaScriptにおける正規表現の進化を振り返り、現代の正規表現の機能を活用するためのヒントを紹介しています。
JavaScriptの正規表現の歴史
JavaScriptに正規表現が初めて導入されたのは1999年に標準化されたECMAScript 3の時代でした。この時点でPerl風の正規表現が採用され、他のPerl系フレーバーと大部分で互換性があるとされていましたが、重要な機能がいくつか欠けていました。その後、JavaScriptの次の標準版であるES5が登場するまでに10年が経過し、その間に他のプログラミング言語や正規表現実装はより強力で読みやすい機能を追加していきました。
ここでは、JavaScriptの正規表現に関するいくつかの重要な改善点を紹介します。
- ES5(2009年):
- リテラルの評価ごとに新しいオブジェクトを作成:以前のバージョンでは、正規表現リテラル(
/pattern/
の形式)を使用するたびに同じオブジェクトが使い回されていた。これにより、特定の状況で意図しない挙動が発生することがあった。ES5では、正規表現リテラルが評価されるたびに新しい正規表現オブジェクトが作成されるようになり、これによってバグの発生が防がれ、予測しやすい動作が保証されるようになった。 - 文字クラス内でエスケープされていないスラッシュの使用が可能に:以前のバージョンでは、文字クラス(
[...]
内)でスラッシュを使用する際に、エスケープが必要だった。例えば、[/]/
のようにスラッシュ自体を含む正規表現を作成する際に問題が発生していた。ES5では、文字クラス内でスラッシュをエスケープせずにそのまま使用できるようになり、正規表現の記述が簡単かつ直感的になった。
- リテラルの評価ごとに新しいオブジェクトを作成:以前のバージョンでは、正規表現リテラル(
- ES6/ES2015:2つの新しいフラグ
y
(スティッキー)とu
(ユニコード)を追加し、正規表現のユニコード関連の改善や厳密なエラー処理を可能にした。 - ES2018:JavaScriptの正規表現に重要な改良を加え、
s
(dotAll)フラグ、後方参照、名前付きキャプチャ、ユニコードプロパティ(\p{...}
および\P{...}
)を追加した。 - ES2020:文字列メソッド
matchAll
を追加し、複数の一致を一度に取得できるようにした。 - ES2022:フラグ
d
(hasIndices)を追加し、一致した部分文字列の開始位置と終了位置を提供する機能を追加。 - ES2024:フラグ
v
(unicodeSets)を導入し、ユニコードプロパティの強化やネストされた文字クラスなど、新しい強力な機能を追加した。
JavaScriptのモダンな正規表現機能の使用方法
ここでは、特に注目すべきいくつかのモダンな正規表現機能を紹介します。
名前付きキャプチャ:名前付きキャプチャグループを使用することで、正規表現が一致した部分文字列をコード内で扱いやすくし、読みやすさと自己文書化を促進します。以下の例では、2つの日付フィールドを持つレコードを正規表現でマッチさせ、キャプチャされた値をオブジェクトとして取得しています。
const record = 'Admitted: 2024-01-01\nReleased: 2024-01-03'; const re = /^Admitted: (?<admitted>\d{4}-\d{2}-\d{2})\nReleased: (?<released>\d{4}-\d{2}-\d{2})$/; const match = record.match(re); console.log(match.groups); /* → { admitted: '2024-01-01', released: '2024-01-03' } */
このコードでは、
Admitted
とReleased
の日付を正規表現で抽出し、名前付きキャプチャグループadmitted
とreleased
を使用して結果をわかりやすく取得しています。後方参照:後方参照は、指定したパターンが一致するかどうかを、文字列の特定の部分に対してのみ確認するために使用されます。以下の例では、「fat」という単語が前にある場合にのみ「cat」という単語を一致させて、置き換えを行っています。
const re = /(?<=fat )cat/g; 'cat, fat cat, brat cat'.replace(re, 'pigeon'); // → 'cat, fat pigeon, brat cat'
この正規表現では、「fat」に続く「cat」だけが「pigeon」に置き換えられます。他の「cat」はそのままです。
matchAll
メソッド:matchAll
メソッドは、正規表現による一致結果をループで処理する際に便利です。このメソッドは一致した詳細な情報を持つイテレータを返し、それをfor...of
ループで簡単に扱うことができます。const re = /(?<char1>\w)(?<char2>\w)/g; for (const match of str.matchAll(re)) { const {char1, char2} = match.groups; console.log(`Matched "${match[0]}" with "${char1}" and "${char2}"`); }
このコードでは、文字列中の連続する2文字をすべて見つけ、そのペアごとに処理を行っています。
matchAll
メソッドを使用することで、複数の一致を効率的に扱うことができます。ユニコードプロパティ:ユニコードプロパティを使用すると、複雑な多言語テキストを精密に処理できます。以下の例では、ギリシャ文字のうち特定の文字を除外したり、ギリシャ文字だけに一致させたりすることができます。
/[\p{Script_Extensions=Greek}--π]/v
この正規表現では、ギリシャ文字のうち「π」以外の文字に一致させています。また、
[\p{Script_Extensions=Greek}&&\p{Letter}]
のように書くことで、ギリシャ文字かつ文字であるものだけに一致させることも可能です。v
フラグ:v
フラグはES2024で追加された新しいフラグで、ユニコードセットや文字クラスのネスト、集合演算(引き算や交差)をサポートします。これにより、正規表現のパターンマッチングの柔軟性が大幅に向上します。// ギリシャ文字のうち「π」を除いたものに一致 const re = /[\p{Script_Extensions=Greek}--π]/v; const result = 'αβγπδε'.match(re); console.log(result); // ['α', 'β', 'γ', 'δ', 'ε']
このコードでは、ギリシャ文字のうち「π」以外の文字をすべてマッチさせています。
v
フラグを使うことで、このような複雑な集合演算を容易に実現できます。
これらの機能を活用することで、JavaScriptの正規表現は他の言語やフレーバーに引けを取らない強力なツールとなる。しかし、依然として読みやすさやメンテナンス性に課題が残る場合もあり、その際にはサードパーティライブラリの利用が推奨される。
詳細はRegexes Got Good: The History And Future Of Regular Expressions In JavaScriptを参照していただきたい。