【実務・中級編】 String.prototype.includes()による判定 – JavaScript実践ガイド

おい、調子はどうだい?
最近、コードレビューをしていて「また昔のクセが出てるな」と思うことがよくあるんだ。例えば、文字列の中に特定のキーワードが入っているかどうかを調べるとき、未だに `indexOf(‘keyword’) !== -1` なんてコードを見かけるたびに、思わずコーヒーカップを置きそうになる。

おいおい、平成の呪縛からはもうとっくに解放されているはずだぜ。令和のフロントエンド開発において、文字列の存在判定の主役は `String.prototype.includes()` だ。

今回は、この `includes()` の基本から、ブラウザの裏側の挙動、そして実務で泥臭く使える実践的なTipsまで、シニアの視点からたっぷりと叩き込んでやる。最後までついてきな。

—

なぜ今さら `includes()` なのか?(`indexOf` との決定的な違い)

まずは基本のおさらいからいこう。`includes()` は、ある文字列の中に指定した部分文字列が含まれているかを `true` か `false` の真偽値で返してくれる非常に素直なメソッドだ。

昔はよくこう書いていたよな。

const message = ‘Hello, Front-end Architecture!’;

// 古臭い書き方(もうやめよう)
if (message.indexOf(‘Front-end’) !== -1) {
console.log(‘含まれています’);
}

この何がダサいかって、「-1じゃないこと」をわざわざ比較しないと真偽値にならないという冗長性だ。人間は認知負荷を下げたい生き物だ。「含まれているか?」を知りたいだけなのに、なぜインデックスの数値(しかも見つからなければ `-1`)なんていう実装都合の値を意識しなきゃいけないんだ?

それをスマートに解決したのが、ES2015(ES6)で導入された `includes()` だ。

const message = ‘Hello, Front-end Architecture!’;

// 現代的で美しい書き方
if (message.includes(‘Front-end’)) {
console.log(‘含まれています!’); // こっちの方が圧倒的に直感的
}

コードの意図がリーダブル(読みやすい)になり、バグの入り込む隙間がグッと減る。これだけでも採用する理由としては十分だろう?

—

ブラウザの裏側で何が起きているのか?

さて、フロントエンドエンジニアなら「動けばいいや」で済ませず、少しだけエンジン側の挙動にも思いを馳せてみてほしい。

V8などの近代的なJavaScriptエンジンにおいて、文字列検索はC++レベルで最適化されている。`includes()` は内部的には `indexOf` と大差ない高速なアルゴリズム(Boyer-Moore法やその変形など、エンジンによって最適化は異なるが)で走っていることが多い。

だが、ここで一つ注意してほしいのが 大文字・小文字の厳密な一致(Case-sensitive) だ。

const role = ‘Administrator’;

console.log(role.includes(‘admin’)); // => false (大文字小文字が違うので弾かれる)

実務の現場では、「ユーザーが入力した検索ワードが大文字だろうが小文字だろうがヒットさせたい」という泥臭い要件が山ほど降ってくる。そんなときは、両者を一度小文字(あるいは大文字)に正規化してから比較するのが鉄則だ。

const role = ‘Administrator’;
const searchTerm = ‘admin’;

// 一度toLowerCase()で小文字に統一して判定する
if (role.toLowerCase().includes(searchTerm.toLowerCase())) {
console.log(‘ヒットしました!’);
}

※ただし、トルコ語のような特殊なロケール(Iとiの扱いが違う)を扱うグローバルなアプリでない限りは、これで大体はうまくいく。

—

実務で効く!開始位置指定(`position`)の活用テクニック

さて、ここからが本題だ。`includes()` の第2引数には、検索を開始するインデックス位置(`position`)を指定できる。これが実務で地味に、かつ絶大な効力を発揮するんだ。

構文はこうだ:
`string.includes(searchString, position)`

例えば、「特定の文字列が、特定の場所より後ろにあるか」を判定したいときや、URLのパス解析、あるいはログ解析などの場面で役立つ。

以下のコードを見てくれ。現場でそのままコピペして使えるように、具体的なユースケースを用意した。

/

  • 実務で使える!URLの特定のプレフィックス以降に特定のパスが含まれているか検証する関数
  • @param {string} url – 検査対象のURL
  • @param {string} keyword – 探したいキーワード
  • @param {number} startIndex – 検索を開始する文字位置

/
function validateUrlPath(url, keyword, startIndex = 0) {
// 第2引数に startIndex を渡すことで、その位置より前にある一致を無視できる
const isFound = url.includes(keyword, startIndex);

if (isFound) {
console.log(`[OK] インデックス ${startIndex} 以降に “${keyword}” が見つかりました。`);
} else {
console.log(`[NG] 条件に一致しません。`);
}
}

// 使用例
const targetUrl = ‘https://example.com/api/v1/users/settings’;

// ドメイン部分(https://example.com)をスキップして、パス部分(/api/…)から検索したい場合
// ‘https://example.com’ の文字数は 19 なので、インデックス 19 から検索を始める
validateUrlPath(targetUrl, ‘users’, 19); // => [OK] 出力される

// 逆に、ドメイン部分にたまたま含まれる文字列を無視したいときなどに応用可能
validateUrlPath(targetUrl, ‘example’, 19); // => [NG] 出力される(19文字目以降には存在しないため)

この「検索開始位置を指定できる」という仕様を知っているだけで、無駄な `slice()` や `substring()` を使って新しい文字列メモリをヒープ上に生成せずに済む。パフォーマンスの微小な最適化だが、こういう細部の積み重ねが、重いSPA(Single Page Application)の動作を軽くする秘訣なんだ。

—

シニアからの警告:`includes()` を使うときの落とし穴

最後に、現場で後輩によくやる失敗談を共有しておこう。

`includes()` は便利だが、「null や undefined の可能性を考慮していない」ケースが後を絶たない。APIから返ってきたデータが想定外に `null` だったりすると、容赦なくアプリがクラッシュする。

// 危険なコード
function hasAccess(user) {
// user.role が null や undefined だった場合、ここで即座にTypeErrorで落ちる
return user.role.includes(‘admin’);
}

TypeScriptを使っていればコンパイルエラーで防げるかもしれないが、JSの現場や型が `any` で汚染されたコードベースでは日常茶飯事だ。
実務では、オプショナルチェイニング(`?.`)や論理演算子を組み合わせて、堅牢(ロバスト)に書くのがプロの作法というものだ。

// 安全で美しいモダンな書き方
function hasAccess(user) {
// オプショナルチェイニングと論理ANDで安全に評価する
// roleが文字列でない場合も考慮して、短絡評価をうまく使う
return user?.role?.includes(‘admin’) ?? false;
}

さらに、もし「配列」の中に特定の要素が含まれているかを調べたい場合も、配列版の `Array.prototype.includes()` が使える。文字列と配列でメソッド名が同じなので、頭の中でごっちゃにならないようにしっかり整理しておいてくれよ。

—

まとめ

文字列操作一つとっても、言語仕様の背景やパフォーマンス、そして実務でのエラーハンドリングまで気を配るのが「できるフロントエンドエンジニア」の条件だ。

今日から `indexOf !== -1` は封印して、スマートな `includes()`、そして必要に応じた開始位置の指定をガンガン活用していってほしい。

君の書くコードが、次のレビューで最高のものになるのを期待しているぜ。それじゃ、また次の現場で!

コメント

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