お疲れ。今日も今日とてレガシーコードの爆弾処理に追われてないか?
フロントエンドの現場にいると、「動くから触るな」の精神で放置された数年前のコードベースに出くわすことが本当によくある。今日も、あるプロダクトの文字列ユーティリティを覗いたら、懐かしの `substr()` が堂々と鎮座していて思わずコーヒーを吹き出しそうになった。
中級からそろそろシニアの背中が見えてくる頃合いの君なら、APIドキュメントの端っこにある「非推奨(Deprecated)」の文字に敏感なはずだ。だが、なぜそのメソッドがダメなのか、裏側で何が起きているのかを説明できるか?
今日は、JavaScriptのレガシーな闇の一つである `String.prototype.substr()` について、仕様の背景からブラウザの裏側の事情、そして現代の正しい代替手段まで、みっちり解説してやろう。
—
なぜ `substr()` は「終わった」メソッドなのか?
まず大前提として、`substr()` は ECMAScript の標準仕様(Language Specification)からすでに外されている。正確に言うと、Web Compatibility(Web互換性)のためにAnnex B(付録B:歴史的理由による非標準機能)という「お蔵入り予備軍の部屋」に押し込まれている状態だ。
`substr(startIndex, length)` の罪深き仕様
思い出してほしい。`substr()` の引数はこうだったな。
- 第1引数:取得開始位置(インデックス)
- 第2引数:文字数(長さ)
これが他の文字列メソッド(`substring()` や `slice()`)と決定的に違うところだ。他の連中は「開始位置」と「終了位置」を取る。この「文字数を指定する」というユニークな仕様こそが、脳内バグの温床になり、長年にわたって多くの開発者をハメてきた。
さらに、マイナス値を入れたときの挙動もカオスだ。
例えば `str.substr(-3, 2)` のようなコード。環境や古い仕様の解釈によって、マイナスインデックスの扱いが微妙に異なり、クロスブラウザでバグる原因になっていた。
ブラウザの裏側で何が起きているのか?
「動くんだからいいじゃん」と思うかもしれないが、JavaScriptエンジニアなら、ブラウザのエンジン(V8やSpiderMonkeyなど)がどう解釈しているかまで想像を巡らせてほしい。
`substr()` のようなレガシーAPIがコード内に残っていると、JIT(Just-In-Time)コンパイラが最適化(Optimization)をかける際、そのコードブロックを「最適化除外リスト」送りや「最適化の恩恵を受けにくい特殊なルート」に分類することがある。
現代のブラウザエンジンは、ECMAScriptの標準に沿ったモダンなメソッド(`slice()` やテンプレートリテラルなど)に対して狂気的なまでの高速化チューニング施しているが、歴史的な例外処理の塊である非推奨メソッドの最適化コストは、エンジン開発者にとっても頭痛の種なのだ。
要するに、`substr()` を使うことは、「わざわざブラウザに古い特殊処理を強要し、実行時パフォーマンスをドブに捨てる行為」に等しい。
—
現場で即座に使える! 現代のベストプラクティス
じゃあ、明日からどう書くべきか。答えはシンプルで、`String.prototype.slice()` を使えばいい。
`slice()` は、配列(Array)の `slice()` と同じメンタルモデルで扱えるため、コードの認知負荷が劇的に下がる。
パターン1:後ろから数えて指定文字数を切り出す(一番よくあるユースケース)
クレジットカードの下4桁マスク処理や、ファイル名の拡張子手前までの切り出しなどでよく使うパターンだな。
// — レガシーな書き方(絶対に真似しちゃいけないやつ)—
const legacyCardNo = “1234567890123456”;
const lastFourLegacy = legacyCardNo.substr(-4); // 「4567」…じゃなくて「3456」か!あぶねえ!
// — 現代の正しい書き方(sliceを使う)—
const cardNo = “1234567890123456”;
// sliceなら第1引数にマイナスを渡せば「末尾からのオフセット」として直感的に動く
const lastFour = cardNo.slice(-4);
console.log(lastFour); // “3456”
`slice(-4)` は、「後ろから4文字目から、文字列の最後まで」という意味になる。文字数ではなく位置を指定している点がポイントだ。
パターン2:開始位置と長さを指定したい場合(substrの完全な置き換え)
どうしても「ここから何文字分ほしいんだ」という要件がある場合は、`slice()` の第2引数に「開始位置 + 欲しい文字数」を計算して渡してやればいい。
const originalText = “FrontendArchitecture”;
// substr(2, 8) = 「インデックス2から8文字分ほしい」場合
const startIndex = 2;
const length = 8;
// sliceで書き換える場合、終了インデックスは「開始位置 + 長さ」になる
const modernText = originalText.slice(startIndex, startIndex + length);
console.log(modernText); // “ontendAr”
最初は「足し算するのめんどくさくない?」と思うかもしれない。だが、コードの可読性と、仕様の揺れがない標準APIを使っているという安心感に比べれば、お釣りが来るほど安い投資だ。
—
シニアからの総括
レガシーなメソッドを叩くことは簡単だが、大事なのは「なぜそれが生まれ、なぜ現代で忌避されるのか」という歴史と仕様のコンテキストを理解することだ。
チームでレガシーコードを見つけたら、ただ「直せ」と言うのではなく、今回話したような「ブラウザの最適化の話」や「認知負荷の話」をサクッと添えてプルリクエストのレビューで教えてあげてほしい。そうやってチーム全体のコード品質を底上げしていくのが、シニアの醍醐味ってやつだ。
さて、コーヒーを飲み干したら、さっそくエディタを開いて `subst..` まで打ってオートコンプリートに出てきたらソッコーで消し去る作業に戻ろうか。頼んだぜ!

コメント