【テクニカル・上級編】 String.prototype.endsWith() – JavaScript実践ガイド

やあ。コードエディタのブルーライトに照らされながら、日夜メモリリークやレンダリングのボトルネックと格闘している同志よ。今日も元気にDOMを叩いているかい?

今回は、JavaScriptの文字列操作において、誰もが一度は使ったことがあるであろう、しかしその内部挙動やアーキテクチャ上の最適化については意外と見落がちなしぶとい奴――`String.prototype.endsWith()` にスポットを当てよう。

「文字列の末尾判定なんて、正規表現か `slice()` で十分だろ」と思ったそこの君。ちょっと待ってほしい。大規模なSPA(シングルページアプリケーション)や、ミリ秒単位のパフォーマンスがコンバージョンレートを左右するEコマースのフロントエンドにおいて、こうした小さなメソッドの選定ミスが、V8エンジンのガベージコレクション(GC)を直撃し、JITコンパイルの効率を静かにむしばんでいく。

今回は、この地味ながら奥深い `endsWith()` の仕様の裏側と、現場で生きる堅牢なアーキテクチャの知見を、ギークな視点から徹底的に解剖していこう。

—

1. `endsWith()` の仕様と、V8エンジンが裏でやっていること

まずは基本のおさらいだ。`str.endsWith(searchString[, length])` は、対象の文字列が `searchString` で終わるかどうかをブール値で返す。第2引数に `length` を取れるのがミソで、これによって文字列全体をスライスし直すことなく、仮想的な文字列の長さで判定を行える。

const filename = “bundle.min.js?v=1.2.3”;

// クエリパラメータを無視して拡張子を判定したいとき、lengthを指定できる
// 文字列全体を操作せず、パフォーマンス上有利
const isJavaScript = filename.endsWith(“.js”, filename.indexOf(“?”));

console.log(isJavaScript); // true

ここでプロフェッショナルとして意識すべきなのは、「余計な新しい文字列インスタンスをヒープ上に生成しない」という点だ。

よくあるアンチパターンとして、以下のようなコードを見かける。

// ❌ 良くない例:slice() は新しい文字列をヒープに割り当てる
if (filename.slice(-3) === “.js”) { … }

`slice()` や `substring()` は、メモリ上に新しいStringオブジェクト(あるいはプリミティブのラッパー)をアロケートする。もしこれが数万件のログデータ処理や、リアルタイムのタイピング補完のようなホットパス(頻繁に実行されるコード領域)で実行された場合、瞬く間にヒープメモリが肥大化し、V8のジェネレーショナルGC(ガベージコレクション)を誘発してメインスレッドを数ミリ秒間ブロックすることになる。

その点、`endsWith()` は、C++で実装されたブラウザの内部エンジンレベルでインデックスの比較を行っているため、不要なメモリアロケーションが発生しない。これぞ、私たちがプリミティブなメソッドを正しく理解し、使いこなすべき理由だ。

—

2. 堅牢なWebアプリケーションのための実用パターン

では、実務の現場で `endsWith()` をどのように活かすべきか。具体的なユースケースを見ていこう。

パターンA:動的なルーティングとファイルパスのバリデーション

フロントエンドのルーターや、プラグイン機構を持つモジュールローダーでは、拡張子や特定のサフィックスによるディスパッチ処理が頻発する。ここで厳密な型チェックと組み合わせることで、意図しないインジェクションやバグを防ぐことができる。

/

  • プラグインファイルが安全に実行可能か検証するアーキテクチャ
  • @param {string} path – 対象のファイルパス
  • @returns {boolean}

/
function validatePluginPath(path) {
// プリミティブ型であることを保証
if (typeof path !== ‘string’) return false;

// 不正なトラバーサルや拡張子の偽装を防ぐ
const allowedSuffixes = [‘.plugin.js’, ‘.umd.cjs’];

// some() と endsWith() の組み合わせによるスケーラブルな判定
return allowedSuffixes.some(suffix => path.endsWith(suffix));
}

// 使用例
console.log(validatePluginPath(“/assets/analytics.plugin.js”)); // true
console.log(validatePluginPath(“/assets/malicious.js.txt”)); // false

パターンB:非同期処理のストリーミングデータにおけるチャンク検証

WebSocketやFetch APIの `ReadableStream` を使って、巨大なテキストデータをチャンク単位で受信している場面を想像してほしい。データの切れ目が特定の区切り文字(あるいはJSONの閉じ括弧など)で終わっているかを判定し、パースのタイミングを制御する際にも `endsWith()` が活躍する。

class StreamBufferProcessor {
constructor() {
this.buffer = ”;
}

/

  • チャンクを受け取り、完全なJSONメッセージとして処理できるか判定
  • @param {string} chunk

/
append(chunk) {
this.buffer += chunk;

// バッファがオブジェクトの閉じ括弧で終わっているかチェック
// ※実際のストリームではエスケープ文字等も考慮が必要だが概念として
if (this.buffer.trim().endsWith(‘}’)) {
this.flush();
}
}

flush() {
try {
const data = JSON.parse(this.buffer);
console.log(“Parsed streamed data:”, data);
this.buffer = ”; // バッファのリセット
} catch (e) {
// パース失敗時のリカバリロジック
console.warn(“Waiting for more complete chunks…”);
}
}
}

—

3. 罠と回避策:Unicodeとサロゲートペアの闇

さて、ここからが本当のエンジニアリングの話だ。JavaScriptの文字列はUTF-16でエンコードされている。これは何を意味するか? サロゲートペア(絵文字や特殊な多言語文字)が含まれる場合、文字数とインデックスの計算が狂うという、JS開発者お馴染みの悪夢が `endsWith()` にもつきまとう。

const str = “Hello 👋”; // 👋 はサロゲートペア(2つのコードユニットを消費)

console.log(str.length); // 8 (H-e-l-l-o-[space]-[high surrogate]-[low surrogate])
console.log(str.endsWith(“👋”)); // true

ここまでは正常に動く。問題は、第2引数の `length` を指定したときだ。

const emojiStr = “A🚀”; // A (1) + 🚀 (2コードユニット = 計3)

// サロゲートペアの途中のインデックスを指定してしまうと、意図しない判定になる
console.log(emojiStr.endsWith(“🚀”, 2)); // false (2文字目までだとロケットの途中までしか読まれない)

もし国際化(i18n)対応や、ユーザーが入力する多様な絵文字・CJK統合漢字などを扱うアプリケーションで厳密な末尾判定を行いたい場合、生の `String.prototype.endsWith()` だけを信じ込んでは痛い目を見る。

堅牢性を極めるための防衛的アプローチ

もしサロゲートペアの分断リスクがある環境で `length` を制御して `endsWith` を使いたい場合は、コードユニット単位ではなく、スプレッド構文やIntl.Segmenterを用いた文字単位の安全なハンドリングをアーキテクチャレベルで強制すべきだ。

/

  • サロゲートペアを考慮した安全なendsWithラッパー
  • @param {string} str
  • @param {string} searchString
  • @param {number} [maxChars] – コードユニットではなく「人間が見る文字数」を指定

/
function safeEndsWith(str, searchString, maxChars) {
if (maxChars !== undefined) {
// 文字単位(Code Point単位)で安全にスライスしてから判定
const chars = Array.from(str);
const sliced = chars.slice(0, maxChars).join(”);
return sliced.endsWith(searchString);
}
return str.endsWith(searchString);
}

パフォーマンスと堅牢性のトレードオフにはなるが、「ユーザー入力」を扱う境界線(Boundary)においては、こうした一手間がプロダクトの品質を何段階も引き上げる。

—

4. まとめ:プロフェッショナルのコードとは

たかが `endsWith()`、されど `endsWith()`。
ただ動くだけのコードを書くのはジュニアエンジニアの仕事だ。私たちシニア・アーキテクトは、その背後にあるメモリ効率、ブラウザの内部挙動、そしてUnicodeという避けられない現実を見据え、「なぜこのメソッドを使うのか」「どこにリスクが潜んでいるのか」を説明できなければならない。

次にコードを書くときは、何気なく打っていたそのメソッドの裏側で、V8エンジンがどう動いているかに少しだけ思いを馳せてみてほしい。

美しいアーキテクチャは、こうした細部へのこだわりから生まれるのだから。

コメント

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