【実務・中級編】 String.prototype.matchAll()によるイテレータ取得 – JavaScript実践ガイド

フロントエンド・スペシャリストの私だ。

おい、そこの君。APIから返ってきた巨大なHTMLやMarkdownのテキストから、特定のパターンをすべて抜き出したい時、どう書いている?まさか、いまだに `String.prototype.match()` にグローバルフラグ(`g`)をつけて呼び出して、「あれ?キャプチャグループが消えたぞ…?」と頭を抱えていたりしないだろうな。

レガシーな `match()` や、無限ループの悪夢を見る `RegExp.prototype.exec()` をぐるぐる回すハックは、もう令和のフロントエンド開発には不要だ。

今回は、JavaScriptの文字列操作における隠れた(いや、もう標準だが)名刀、`String.prototype.matchAll()` について徹底的に解説しよう。これを使えば、君のコードベースから「正規表現の闇」が綺麗に消え去るはずだ。

—

なぜ `matchAll` なのか? ── 従来の文字列探索が抱えていた「泥臭い歴史」

まず、敵を知るために過去の過ちを振り返ろう。
正規表現で文字列中から複数のマッチを取得したい場合、我々は長年 `String.prototype.match()` を使ってきた。しかし、ここに一つの大きな罠があった。

正規表現にグローバルフラグ(`g`)を付与した瞬間、`match()` はマッチした文字列の配列(Array)を返すだけで、各マッチに含まれるはずのキャプチャグループ(括弧で囲んだ部分)を綺麗さっぱり無視する仕様になっていたのだ。

const text = “Item: Apple ($10), Item: Banana ($20)”;
const regex = /Item: (\w+) \((\$\d+)\)/g;

// グローバルフラグをつけると…
console.log(text.match(regex));
// 出力: [“Item: Apple ($10)”, “Item: Banana ($20)”]
// ああ!グループ1の “Apple” や、グループ2の “$10” が取れない!

これまでは、これを回避するために `RegExp.prototype.exec()` を `while` ループで回すという、非常に泥臭いボイラープレートを書く必要があった。

// 昔ながらの「execループ」(もう書きたくない)
const regex = /Item: (\w+) \((\$\d+)\)/g;
let match;
while ((match = regex.exec(text)) !== null) {
console.log(match[1], match[2]); // ここでやっと取れる
}

このコードを見るだけで胃が痛くなるだろう。状態を持つ `regex` オブジェクトをループ外で回し、`null` チェックをしながら回す。バグの温床でしかない。

そこで登場したのが、ES2020で策定された `matchAll()` だ。

—

`matchAll()` の基本仕様と裏側の仕組み

`String.prototype.matchAll()` は、指定した正規表現に一致するすべての結果を、「イテレータ(Iterator)」として返す。

ここでブラウザの裏側の話をしておこう。
なぜ「配列」ではなく「イテレータ」なのか? 答えはメモリ効率とパフォーマンスだ。

もし、数メガバイトある巨大なログファイルやブログ記事のテキストから、数万件に及ぶ正規表現マッチを一度に配列としてメモリ上に展開したらどうなるか? メモリを圧迫し、メインスレッドをブロックして画面がカクつく(Jankの発生)原因になる。

イテレータであれば、「次に必要になった瞬間(オンデマンド)」に1つずつマッチ結果を評価・生成するため、巨大な文字列を扱ってもメモリフットプリントを最小限に抑えられるのだ。シニアエンジニアとして、こういう「見えない最適化」への配慮には敏感であってほしい。

—

現場で即戦力になる実践コード

百聞は一見に如かず。実務でよくあるユースケースを想定したコードを見てみよう。
今回は、「CMSから渡ってきたHTML文字列の中から、特定のカスタムタグを抽出し、その属性値と中身をごっそり構造化データに変換する」というシチュエーションだ。

以下のコードをそのままブラウザのコンソールやNode.js環境に貼り付けて動かしてみてくれ。

/

  • 記事本文から特定のカスタム記法(例: @mention や #hashtag、あるいはカスタムコンポーネント)を抽出する関数
  • @param {string} content – 検索対象のテキスト

/
function parseCustomComponents(content) {
// グローバルフラグ ‘g’ が必須!忘れると TypeError が投げられるので注意。
// キャプチャグループを使って、コンポーネント名と属性値を綺麗に分ける
const componentRegex = /\[card\s+id=”([^”]+)”\s+theme=”([^”]+)”\]/g;

// matchAll() はイテレータを返すため、スプレッド構文や for…of で展開できる
// 今回は全件を一気に処理して配列にしたいので […matchAll()] で受ける
const matches = […content.matchAll(componentRegex)];

if (matches.length === 0) {
console.log(‘一致するカスタムコンポーネントはありませんでした。’);
return [];
}

// 抽出結果を綺麗にマッピング
return matches.map((match, index) => {
// match[0] はマッチした文字列全体
// match[1] 以降はキャプチャグループ(カッコの中身)
const [fullMatch, cardId, theme] = match;

console.log(`[デバッグ] ${index + 1つ目のマッチ}:`, {
matchedString: fullMatch,
indexInText: match.index, // 文字列内のどこで見つかったかのインデックス
inputString: match.input // 検索対象の元文字列
});

return {
id: cardId,
theme: theme,
raw: fullMatch,
position: match.index
};
});
}

// — テスト実行 —
const articleBody = `
本日は新しいフロントエンドのアーキテクチャについて解説します。

[card id=”post-101″ theme=”dark”]

途中に通常の段落を挟みつつ、もう一つのカードも配置します。

[card id=”post-202″ theme=”light”]
`;

const parsedData = parseCustomComponents(articleBody);
console.log(‘最終的な構造化データ:’, parsedData);

このコードの美しさとポイント

1. キャプチャグループが完全に生きている: `match[1]` や `match[2]` で、`id` や `theme` の値がピンポイントで取れているのがわかるはずだ。
2. メタ情報へのアクセス: 各結果オブジェクトには `match.index`(元文字列の何文字目で見つかったか)や `match.input` が含まれている。例えば「エラー箇所をハイライトする」ようなUIを作る時、このインデックス情報が猛烈に効いてくる。
3. スプレッド構文との相性: `[…str.matchAll(regex)]` と書くことで、イテレータを通常の配列に変換できる。配列のメソッド(`map`, `filter` など)チェインに持ち込めるため、コードが非常に宣言的で読みやすくなる。

—

チーム開発における注意点(シニアからの警告)

最後に、現場でこの `matchAll` を使う際の「落とし穴」をいくつか共有しておこう。

1. グローバルフラグ(`g`)の付け忘れ
`matchAll()` は、グローバルフラグが設定されていない正規表現を渡すと、容赦なく `TypeError` を投げつけてくる。
「あれ?動かないな」と思ったら、大体レギュラーエクスプレッションの末尾に `g` を付け忘れている。TypeScriptを書いているなら型推論で気づきやすいが、プレーンなJSではRuntimeエラーになるので注意しろ。

2. 古い環境への配慮(Babel/Polyfill)
ECMAScript 2020(ES11)以降の標準仕様であるため、現代のモダンブラウザやNode.js(v12以降)であれば完全にネイティブサポートされている。もし、社内のインフラ事情でまだ骨董品のような古いブラウザをサポートしなければならないプロジェクトであれば、Core-jsなどのポリフィルが必須になる。その点はビルドパイプラインの構成を確認してくれ。

—

まとめ

文字列操作は、フロントエンドエンジニアの基礎体力が如実に現れる領域だ。
「動けばなんでもいいや」と泥臭い `exec` ループや無理矢理なスライスを重ねるのではなく、標準仕様である `String.prototype.matchAll()` をスマートに使いこなし、パフォーマンスが高く、かつ他のメンバーが読んでも一発で意図が伝わる美しいコードを書こう。

君の次のプルリクエストを楽しみにしている。

コメント

タイトルとURLをコピーしました