【実務・中級編】 String.prototype.lastIndexOf()による逆順検索 – JavaScript実践ガイド

やあ。最近、コードレビューをしていて「あ、これ惜しいな」「もっと綺麗に書けるのに」と思うことがよくあるんだよね。特に文字列操作あたりは、なんとなく`indexOf()`や正規表現を引っ張り出してきて、動くには動くけど……というコードに出会うことが多い。

今回は、文字列の末尾から前方に向かって特定のキーワードを探す`String.prototype.lastIndexOf()`を取り上げる。

「え、今更 `lastIndexOf`?」って思ったかい?
実はこの地味なメソッド、フロントエンドの実務で「ファイルの拡張子をスマートに抜きたい」「URLのパスから最後のセグメントだけを正確に切り出したい」といった場面に直面したとき、知っているかいないかでコードの美しさとパフォーマンスが大きく変わる隠れた実力者なんだ。

今日は、こいつの仕様の裏側から、現場で即戦力になるテクニックまで、シニアの視点でたっぷりと解説していこう。

—

1. `lastIndexOf()` ってぶっちゃけどんな仕様?

`lastIndexOf()` は、指定した文字列を「右から左へ(末尾から前方へ)」検索し、最初に見つかった位置(インデックス)を数値で返すメソッドだ。

基本の構文はこうだ。

str.lastIndexOf(searchValue[, fromIndex])

  • `searchValue`: 探したい文字列。
  • `fromIndex`(オプション): 検索を開始するインデックスの上限。デフォルトは文字列の長さ(`str.length – 1`)。

お馴染みの `indexOf()` が「左から右」に探すのに対して、`lastIndexOf()` は「右から左」に探す。ただ、ここで一つ初心者がハマりがちな重要な仕様の罠がある。

「右から左へ検索する」とは言うものの、返されるインデックスの番号自体は、あくまで「左端を0とした通常のインデックス」であるということだ。

これ、言葉だけだとピンとこないよな。ちょっと次のコードを見てほしい。

const text = “frontend.development.js”;

// 右から ‘.’ を探す
console.log(text.lastIndexOf(‘.’)); // 19

末尾の `.js` の手前にあるドットの位置を綺麗に一発で射抜いている。右から探し始めたけれど、返ってきた `19` という数字は、左端の `f` を `0` と数えたときの位置だ。ここを勘違いして「右からの距離」だと誤解していると、後続の `slice` 処理などで盛大にバグるから注意してくれ。

—

2. ブラウザの裏側で何が起きているのか?(V8エンジンの視点)

フロントエンドエンジニアとして一歩先に行くなら、「JavaScriptのメソッドが裏側でどう動いているか」のイメージを持っておくべきだ。

V8などの近代的なJavaScriptエンジンにおいて、Stringオブジェクトは内部的にはUTF-16(またはラテン1文字なら1バイト)の連続したメモリ領域(バッファ)として保持されている。

`indexOf()` は、文字列の先頭(インデックス0)から1文字ずつインクリメントしながらメモリをスキャンしていく。一方、`lastIndexOf()` は、指定された `fromIndex` からデクリメント(減算)しながらメモリを逆向きにスキャンしていくんだ。

ここで実務的なパフォーマンスの話をしよう。
「検索対象の文字列がめちゃくちゃ長い(例えば数万文字のHTMLやログデータ)」かつ「探したいキーワードが末尾の方にある」と分かっている場合、`indexOf()` を使うと先頭からスキャンし始めるため無駄な走査が発生する。
しかし、`lastIndexOf()` であれば、末尾付近の `fromIndex` を指定してやれば、一瞬でターゲットにヒットさせることができる。

逆に、ターゲットが先頭付近にあると分かっているのに `lastIndexOf()` を使うと、無駄に末尾から遡るハメになる。「どこに目的のデータがありそうか」というドメイン知識に合わせて使い分けるのが、プロの選択というわけだ。

—

3. 現場で使える!実践的なコードパターン

理屈はこれくらいにして、実際のフロントエンド開発で「おっ、これ使えるな」と思えるユースケースを3つ紹介しよう。そのままエディタにコピペして動かせるようにしてある。

パターンA:ファイル名から拡張子を安全に抜き出す

ユーザーがアップロードしたファイル名(例: `my.awesome.profile.image.png`)から拡張子を抽出する処理によく使われる。

/

  • ファイル名から拡張子を取得する関数
  • @param {string} filename
  • @returns {string} 拡張子(ドットを含まない)

/
function getFileExtension(filename) {
// 最後に現れるドットの位置を探す
const lastDotIndex = filename.lastIndexOf(‘.’);

// ドットが見つからない場合や、ファイル名の先頭にある場合(隠しファイル等)
if (lastDotIndex === -1 || lastDotIndex === 0) {
return ”;
}

// ドットの次から末尾までを切り出す
return filename.slice(lastDotIndex + 1);
}

// 実行例
console.log(getFileExtension(‘archive.tar.gz’)); // “gz”
console.log(getFileExtension(‘index.html’)); // “html”
console.log(getFileExtension(‘README’)); // “”

正規表現を使う方法もあるが、単純な文字列の末尾からの切り出しであれば、`lastIndexOf` の方が可読性が高く、意図せぬReDoS(正規表現DoS攻撃)のリスクも排除できる。こういう地味で堅牢なコードが、プロダクション環境では重宝されるんだ。

パターンB:URLのパスから最後のセグメント(IDなど)を取得する

SPA(Single Page Application)のルーティングやAPIのレスポンス処理で、`/api/v1/users/42` のようなRESTfulなパスから最後のID(`42`)を取り出したいときがある。

/

  • URLパスの最後のセグメントを取得する
  • @param {string} path
  • @returns {string}

/
function getLastPathSegment(path) {
// 末尾のトレイリングスラッシュ(末尾のスラッシュ)を考慮して正規化
const trimmedPath = path.endsWith(‘/’) ? path.slice(0, -1) : path;

const lastSlashIndex = trimmedPath.lastIndexOf(‘/’);

if (lastSlashIndex === -1) {
return trimmedPath;
}

return trimmedPath.slice(lastSlashIndex + 1);
}

// 実行例
console.log(getLastPathSegment(‘/api/v1/products/1089/’)); // “1089”
console.log(getLastPathSegment(‘/dashboard’)); // “dashboard”

末尾にスラッシュがつくケースを `endsWith` でケアしつつ、`lastIndexOf(‘/’)` で最後のスラッシュの位置を特定する。実務のAPI処理では定番のイディオムだね。

パターンC:第2引数(fromIndex)を活かした「特定の位置より前」からの検索

`lastIndexOf` の真骨頂は、第2引数の `fromIndex` を使いこなすことにある。例えば、「特定のキーワードよりさらに前にある、最後のスラッシュを見つけたい」といった複雑なパース処理で威力を発揮する。

const logMessage = “[INFO] 2023-10-25 /var/www/html/index.js – Process completed successfully”;

// 「index.js」という文字列の位置をまず特定
const targetIndex = logMessage.indexOf(‘index.js’);

if (targetIndex !== -1) {
// 「index.js」の出現位置よりも「前」の範囲だけで、最後のスラッシュを探す
const lastSlashBeforeIndex = logMessage.lastIndexOf(‘/’, targetIndex);

console.log(lastSlashBeforeIndex); // /var/www/html/ の最後のスラッシュの位置を指す

// パス部分だけを切り出す
const extractedPath = logMessage.slice(lastSlashBeforeIndex – 3, targetIndex); // 簡易的な切り出し
console.log(extractedPath); // “html/” あたりが取れる
}

このように、`indexOf` で見つけた位置を `lastIndexOf` の第2引数に渡すことで、「この地点から左へ遡った最初の一致」をピンポイントで狙い撃ちできる。このテクニックを知っていると、独自の簡易パーサーやログ解析ツールを自作するときにめちゃくちゃ役立つはずだ。

—

シニアからのまとめ

JavaScriptの標準メソッドは、どれも一見するとシンプルで、「ググれば使い方はすぐわかる」ものばかりだ。しかし、今回紹介した `lastIndexOf()` のように、仕様の細かいニュアンス(インデックスの数え方、`fromIndex` の挙動、メモリ走査の方向)を深く理解していると、コードのパフォーマンスや安全性、そして何より「不測のバグを生みにくい設計」が自然とできるようになる。

「なんとなく動く」から「理由を持ってこれを選ぶ」へ。
日々のコードにそんなプロのこだわりを持っていこうぜ。さて、次のタスクに取りかかろうか。

コメント

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