やあ、よく来てくれた。現場の最前線でコードを書き続けている君なら、古びたコードベースの片隅で、あるいは無意識に叩いたエディタの補完候補の中に、その「幽霊」を見かけたことがあるはずだ。
そう、`String.prototype.substr()` のことだ。
「動いているならいいじゃないか」という声が聞こえてきそうだが、アーキテクトの視点はそこにはない。我々が守るべきは、今この瞬間の動作だけでなく、数年後の保守性、実行エンジンの最適化パス、そして何より「意図が明確で堅牢なコード」だ。
今日は、なぜこの使い慣れたはずの `substr()` を捨て去るべきなのか、そしてその先にどのような設計思想を持つべきなのか。V8エンジンの内部挙動や、ECMAScriptの歴史的な背景を交えて、深く、泥臭く語ってみよう。
—
1. 規格の「日陰」に追いやられた Annex B の住人
まず、残酷な真実を突きつけよう。`String.prototype.substr()` は、モダンな ECMAScript の仕様において、いわゆる Annex B (Additional ECMAScript Features for Web Browsers) に分類されている。
これはどういう意味か。簡単に言えば「Webブラウザとの互換性のために渋々残しているが、新しいコードで使うことは全く推奨されない、標準の『外側』にある機能」ということだ。
JavaScriptという言語は、過去の負の遺産を非常に大切にする。一度公開されたAPIを削除することは、Webを壊すことを意味するからだ。しかし、`substr()` はもはや標準化のメインストリームからは外れている。これを使っているという事自体が、アプリケーションの技術的負債を少しずつ、だが確実に積み上げている証拠なのだ。
2. なぜ `substr()` は設計ミスだったのか
最大の問題は、その引数の設計にある。
// 第2引数が「終了位置」ではなく「長さ」
const text = “JavaScript Architecture”;
const result = text.substr(11, 12); // “Architecture”
一見便利に見える。しかし、JavaScriptの他の多くのメソッド(`slice`, `substring`, `Array.prototype.slice` など)は、一貫して `(start, end)` というインデックスベースのインターフェースを採用している。
大規模なプロジェクトにおいて、この「一貫性の欠如」は致命的なバグの温床だ。深夜のトラブルシューティング中、疲弊したエンジニアの脳が「第2引数は終了インデックスだ」と誤認した瞬間、オフバイワンエラー(1の誤差によるバグ)が発生する。
アーキテクチャの観点から言えば、「例外的な振る舞いをするAPI」をコードベースに混ぜることは、認知負荷を無駄に高める行為に他ならない。
3. `slice()` vs `substring()`:どちらを選ぶべきか
`substr()` を廃止する際、我々の前には2つの選択肢が現れる。`slice()` と `substring()` だ。ここで思考を止めてはいけない。結論から言えば、現代のフロントエンド・アーキテクチャにおいては `slice()` 一択だ。
その理由を、内部挙動と仕様の違いから紐解いてみよう。
負のインデックスの扱い
これが最も大きな分水嶺だ。
const code = “SYS-999-PROD”;
// sliceの場合:末尾からのオフセットとして機能する(直感的)
console.log(code.slice(-4)); // “PROD”
// substringの場合:負の数は 0 として扱われる(古い設計)
console.log(code.substring(-4)); // “SYS-999-PROD”
`slice()` は `Array.prototype.slice` と全く同じ挙動を示す。この「配列操作との対称性」こそが、堅牢なデータ処理パイプラインを構築する上で極めて重要だ。逆に `substring()` の「引数が負なら0、引数の前後が逆なら入れ替える」というお節介な挙動は、意図しないバグを隠蔽してしまうリスクがある。
4. エンジン内部とメモリ効率の視点
ここで少し、V8エンジンなどのブラウザ内部の動きに触れておこう。
JavaScriptの文字列はイミュータブル(不変)だ。文字列の一部を切り出す際、エンジン内部では必ずしも新しいメモリ領域を確保して文字列をコピーしているわけではない。
多くのモダンなJavaScriptエンジンは “Sliced Strings” という最適化手法を用いる。これは、新しい文字列オブジェクトを作る代わりに、「元の巨大な文字列への参照」と「開始位置・長さ」だけを保持する仕組みだ。
`substr()`, `slice()`, `substring()` のどれを使っても、現代のエンジンなら同様の最適化が行われる。しかし、`substr()` は Annex B という「非主流」のパスを通る。将来的なエンジン最適化の優先順位において、主流である `slice()` が常に優遇されるのは自明の理だ。
また、非同期処理が複雑に絡み合う現代のアプリケーションにおいて、文字列操作のパフォーマンス差がミリ秒以下であったとしても、コードの予測可能性(Predictability)がもたらす「デバッグコストの削減」というメリットは、どんな微細な最適化よりも価値がある。
5. 実務的なリファクタリング・ガイド
では、現場のコードをどう書き換えるべきか。単なる置換では危険だ。意味論を正しく理解し、安全に移行するためのコード例を示そう。
/
- 文字列抽出のモダン化戦略
/
const rawData = “TOKEN_20231027_RESERVED”;
// — BEFORE: substr (非推奨・レガシー) —
// 第2引数は「抽出する文字数」
const legacyDate = rawData.substr(6, 8);
// — AFTER: slice (推奨・モダン) —
// 第2引数は「終了インデックス」
// 計算式: start + length = end (6 + 8 = 14)
const modernDate = rawData.slice(6, 14);
/
- 現場でよくある「末尾からn文字」のケース
/
// substrの場合
const lastFourOld = rawData.substr(-8); // 実は負の数も一部で動くが、挙動がブラウザ依存だった過去がある
// sliceの場合(圧倒的にクリーンで安全)
const lastFourNew = rawData.slice(-8);
/
- 徹底した安全性の確保
- 動的な長さを扱う場合は、ヘルパー関数にロジックを閉じ込める
/
const getSubstringByLength = (str, start, length) => {
// sliceを用いて substr の挙動を安全にシミュレートする
return str.slice(start, start + length);
};
執筆後記:アーキテクトとしての矜持
`String.prototype.substr()` を使い続けることは、今日明日でシステムを崩壊させるような致命傷にはならないかもしれない。しかし、細部へのこだわりを捨てた瞬間に、コードの品質は腐敗し始める。
「なぜ `slice` を使うのか?」という問いに対し、「ECMAの Annex B を避け、配列操作との一貫性を保ち、負のインデックスによる予測可能性を担保するためです」と答えられるエンジニア。それこそが、単なるコーダーではなく、システム全体の健全性を司るアーキテクトだと私は思う。
古い地図(substr)を捨て、より正確な測量計(slice)を手に取ろう。その一歩が、10年後もメンテナンス可能な美しいコードベースへの道標となるはずだ。
幸運を。良いコードを書いてくれ。

コメント