お疲れ様!今日のテーマは、誰もが一度は使ったことがある、でも実は仕様の「裏の顔」を正しく把握していないと本番環境で不意のランタイムエラー(特にあの悪名高き `TypeError: Cannot read properties of null`)を引き起こしやすい、`String.prototype.match()` と正規表現の基本についてだ。
「正規表現で引っかかった文字列を取り出すだけでしょ?」と軽く考えていると、思わぬところで足をすくわれる。今日は、ブラウザの裏側の挙動から、実務で絶対にやらかさないための防衛策、そしてパフォーマンスを意識した書き方まで、フロントエンドの現場で本当に必要な知識を叩き込んでいくよ。準備はいいかい?
—
1. `match()` の基本仕様と「null」という最大の罠
まずは基本中の基本、そして最大の罠から。
`match()` は、対象の文字列に対して正規表現を適用し、マッチした結果を配列、またはマッチしなかった場合に `null` で返すメソッドだ。
ここで絶対に頭に叩き込んでほしいのは、「マッチしない場合は空配列 `[]` ではなく `null` が返る」という点。
const text = “Hello, JavaScript World!”;
// ‘Python’ は含まれていないので、result は null になる
const result = text.match(/Python/);
// ✕ 危険! null に対して destructing(分割代入)やプロパティアクセスをすると即死する
// TypeError: Cannot read properties of null (reading ‘0’)
const [matched] = result;
実務で急いでコードを書いていると、つい「マッチする前提」で配列として扱ってしまい、本番環境でユーザーが想定外の入力をした瞬間に画面が真っ白になる……なんて障害が後を絶たない。
これを防ぐための現代的なベストプラクティスは、オプショナルチェイニング(`?.`)やNull合体演算子(`??`)を組み合わせた「防衛的ガード」だ。
// ◯ 安全な書き方:null だった場合は空配列をデフォルト値にする
const [matched = “”] = text.match(/Python/) ?? [];
console.log(matched); // “” (安全にハンドリングできる)
まずはこの「`null` が返りうる」という仕様を、無意識レベルでケアできるようになろう。
—
2. ブラウザの裏側で何が起きているか?
ここで少し、ブラウザ(V8エンジンなど)の裏側の挙動に目を向けてみよう。
私たちが `match()` を呼び出したとき、JavaScriptエンジンは単に文字列を左から右へ舐めるように探しているわけではない。
1. 正規表現のコンパイル:
`/pattern/` というリテラルは、実行時に「正規表現オブジェクト(RegExp)」にコンパイルされる。
2. バックトラッキング(逆戻り解析):
正規表現エンジン(多くの場合は NFA: 非決定性有限オートマトン)は、パターンにマッチするかどうかを試行錯誤しながら探索する。もし複雑なパターン(特に `(a+)+` のようなネストされた量指定子)に対してマッチしない長い文字列を渡すと、エンジンは膨大な数の組み合わせを「バックトラック(探索のやり直し)」することになり、最悪の場合、ブラウザのスレッドをフリーズさせる(ReDoS: 正規表現サービス拒否攻撃)。
だからこそ、実務では「不必要に複雑な正規表現を書かないこと」と同時に、「単に文字列が含まれているかチェックしたいだけなら `match()` を使わない」という鉄則が生まれるんだ。
- 単なる存在チェック ➜ `String.prototype.includes()` の方が圧倒的に高速。
- 正規表現による存在チェックのみ ➜ `RegExp.prototype.test()` を使う。
`match()` は、「マッチした『中身(文字列)』をどうしても抽出したいときだけ」 使う贅沢な処理だと覚えておいてほしい。
—
3. `g`(グローバル)フラグの有無による「挙動の激変」を理解する
`match()` の設計で最もトリッキーなのが、`g`(global)フラグがあるかないかで、戻り値の配列の構造が180度変わるという点だ。多くの若手がここで混乱する。
① `g` フラグ「なし」の場合:キャプチャグループを含む詳細情報
`g` フラグをつけない場合、`match()` は最初に見つかった1件だけを返す。ただし、返ってくる配列はただの配列ではなく、インデックスやキャプチャグループ(丸括弧 `()` で囲った部分)の情報を含んだ「特殊な配列」になる。
② `g` フラグ「あり」の場合:マッチした全文字列の単純な配列
`g` フラグをつけると、文字列全体からマッチするものをすべて掻き集めて配列にしてくれる。一見便利だが、キャプチャグループなどの詳細情報はすべて捨て去られ、ただのマッチした文字列の平坦な配列になる。
言葉で言うより、コードを見た方が早いね。
const targetText = “ID: US-1234, ID: JP-5678”;
// —————————————————-
// パターンA: gフラグなし(最初の1件の詳細情報を取る)
// —————————————————-
const resultWithoutG = targetText.match(/ID: ([A-Z]{2})-(\d{4})/);
console.log(resultWithoutG);
/
出力結果: [
‘ID: US-1234’, // 0: マッチした全体
‘US’, // 1: キャプチャグループ1 ([A-Z]{2})
‘1234’, // 2: キャプチャグループ2 (\d{4})
index: 0, // マッチした開始位置
input: ‘ID: US-1234, ID: JP-5678’, // 元の文字列
groups: undefined // 名前付きキャプチャグループ用
]
/
// —————————————————-
// パターンB: gフラグあり(すべて抽出するが、詳細は捨てる)
// —————————————————-
const resultWithG = targetText.match(/ID: ([A-Z]{2})-(\d{4})/g);
console.log(resultWithG);
/
出力結果: [
‘ID: US-1234’,
‘ID: JP-5678’
]
// ※ ‘US’ や ‘1234’、index などの情報はすべて消滅している!
/
「グローバルマッチしたいけど、キャプチャグループも欲しい!」ときは?
実務では「すべてのマッチを取りつつ、それぞれのキャプチャグループ(上記の例でいう国コード `US` や `JP`)も個別に取得したい」という要件が頻出する。
昔のJavaScriptはこれをやるために `RegExp.prototype.exec()` を `while` ループで回すという、非常に泥臭く可読性の低いコードを書く必要があった。
しかし、モダンJavaScript(ES2020以降)には救世主がいる。それが `String.prototype.matchAll()` だ。
// matchAll を使えば、gフラグ付きで詳細情報をイテレータとして取得できる
const matches = targetText.matchAll(/ID: ([A-Z]{2})-(\d{4})/g);
for (const match of matches) {
console.log(`国コード: ${match[1]}, 番号: ${match[2]}`);
}
// 出力:
// “国コード: US, 番号: 1234”
// “国コード: JP, 番号: 5678”
実務で「`g` フラグ付きでキャプチャしたい」となったら、迷わず `matchAll()` を選ぶようにしよう。
—
4. 現場で今すぐ使える実践コード
ここからは、コピペしてそのままプロダクションコードに組み込めるレベルの実践的なサンプルを紹介する。
実例1: URLから特定のクエリパラメータを安全に抽出する(`g` なし・キャプチャグループ)
実務でよくある、特定のパターンから部分文字列を抜き出す処理だ。オプショナルチェイニングを使って、該当しない場合も安全に `undefined` を返すように設計している。
/
- URLから特定のクエリパラメータの値を抽出する(簡易版)
- @param {string} url
- @param {string} key
- @returns {string|undefined}
/
function getQueryParam(url, key) {
// 動的に正規表現を生成(エスケープ処理は簡易化しています)
// 例: [?&]userId=([^&]+)
const regex = new RegExp(`[?&]${key}=([^&]+)`);
const match = url.match(regex);
// match[1] には1つ目のキャプチャグループ(値の部分)が入る
// match が null の場合はオプショナルチェイニングで安全に undefined を返す
return match?.[1];
}
const testUrl = “https://example.com/api/v1/users?userId=usr_88392&track=direct”;
console.log(getQueryParam(testUrl, “userId”)); // “usr_88392”
console.log(getQueryParam(testUrl, “token”)); // undefined (エラーにならず安全)
実例2: テキスト内からSNSの「ハッシュタグ」をすべて抽出する(`g` あり)
ユーザーが投稿したテキストから、`#` から始まるハッシュタグをすべて抜き出す処理。重複排除(`Set`)まで行うのが実務レベルの鉄板パターンだ。
/
- テキストからハッシュタグ(#日本語・英語)をすべて抽出する
- @param {string} text
- @returns {string[]} 重複のないハッシュタグの配列
/
function extractHashtags(text) {
// #の後に、スペースや記号以外の文字が1文字以上続くパターン
// g フラグでテキスト全体からすべて抽出する
const hashtagRegex = /#[\p{L}\p{N}_]+/gu;
const matches = text.match(hashtagRegex);
// マッチしない場合は null になるため、空配列でフォールバック
if (!matches) {
return [];
}
// 重複を排除して返す
return […new Set(matches)];
}
const postText = “今日は #JavaScript の勉強! #Frontend 開発は楽しいね。 #JavaScript 最高!”;
console.log(extractHashtags(postText));
// 出力: [ ‘#JavaScript’, ‘#Frontend’ ]
—
5. パフォーマンスを意識したシニアのひと知恵
最後に、一歩先を行くシニアエンジニアとして意識してほしいパフォーマンスのベストプラクティスを共有しておく。
正規表現オブジェクトをループ内で生成しない
もし関数の中で `match()` を使い、その関数がループ(`map` や `forEach`)の中で何百回も実行される場合、関数の内部で正規表現リテラルを書いてはいけない。
// ✕ バッドプラクティス: 関数が呼ばれるたびに正規表現が再評価・コンパイルされる
function validateEmail(text) {
return text.match(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/);
}
// ◯ グッドプラクティス: スコープの外側で一度だけコンパイルしておく
const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
function validateEmailOptimized(text) {
return text.match(EMAIL_REGEX);
}
静的なパターンであれば、モジュールスコープ(関数の外)で定数として一度だけ定義しておくのが、メモリ効率的にも実行速度的にも大原則だ。
—
まとめ
`String.prototype.match()` は非常に強力だが、その挙動は `g` フラグの有無によって大きく変わり、不一致時には `null` を返すという「クセ」を持っている。
1. 基本は `match()?.[1]` や `?? []` で `null` 安全を徹底すること
2. 複数のマッチから詳細情報(グループ)も抜きたいなら `matchAll()` を使うこと
3. 単なる存在チェックは `includes()` や `test()` で済ませ、CPUに優しくすること
この3つを意識するだけで、君が書くコードの堅牢性とパフォーマンスは劇的に向上する。明日からのコードレビューや実装で、ぜひこの視点を取り入れてみてほしい。頼りにしてるよ!

コメント