さらば、レガシー:`String.prototype.substr()`があなたのコードベースを腐らせる理由と、現代の文字列操作戦略
こんにちは。日夜フロントエンドのコードベースと格闘し、JavaScriptエンジンがV8の中でどうやってバイトコードを解釈しているかに思いを馳せるのが好きな、ちょっと厄介なアーキテクトです。
さて、コードレビューをしていて、未だに以下のようなコードを見かけて冷や汗をかいたことはないでしょうか?
// レガシーなコードの残骸
const id = user.id.substr(0, 8);
一見すると、何気ない文字列の切り出し処理です。「動いているんだからいいじゃないか」と思われるかもしれません。しかし、フロントエンド・スペシャリストの視点から言わせてもらえば、この `substr()` がコードベースに残っていること自体が、技術的負債の蓄積を示す赤信号であり、場合によってはパフォーマンスや互換性の地雷を踏み抜く原因になります。
今回は、なぜ `String.prototype.substr()` がECMAScriptの仕様から「オワコン(非推奨:Legacy feature)」扱いされ、現代のWebアプリケーションにおいて完全に排除されるべきなのか。その背景にあるブラウザエンジンの内部事情や、堅牢なアーキテクチャの観点から徹底的に解剖していきましょう。
—
1. `substr()` の何が問題なのか?仕様の闇と歴史的背景
まず大前提として、`String.prototype.substr()` は、`substring()` や `slice()` とは似て非なる、歴史的な異端児です。
- `slice(startIndex, endIndex)`: 開始位置と終了位置を指定する(モダンな標準)。
- `substring(startIndex, endIndex)`: 開始位置と終了位置を指定する(引数の大小関係を自動で入れ替えるお節介仕様)。
- `substr(startIndex, length)`: 開始位置と切り出す文字数(Length)を指定する。
この「第2引数に長さを取る」という仕様が、ECMAScriptの標準規格(ES3)から長らく外れ、B1(Browser-specific)機能として長い間ゾンビのように生き残ってきました。正式にECMAScript(Annex B)に取り込まれたのも、Webの互換性を維持するための「やむを得ない特例措置」に過ぎません。
そして仕様書(ECMA-262)において、現在 `substr()` は Normative Optional(実質的な非推奨・削除可能カテゴリ) に位置づけられています。つまり、V8やSpiderMonkeyなどのモダンJSエンジンにとって、「いつ仕様から完全にパージされても文句は言えない厄介なレガシーAPI」なのです。
—
2. アーキテクチャとパフォーマンスの観点からの懸念
「動くなら別にいいのでは?」という甘い考えを粉砕するために、もう少し踏み込んだ話をしましょう。私たちが構築する現代のWebアプリケーションは、SSR(サーバーサイドレンダリング)やクライアントサイドでの数万件規模のDOM差分計算、リアルタイムなデータバインディングなど、メモリとCPUに高負荷な処理が常態化しています。
メモリ効率とV8エンジンにおける文字列の扱い
V8エンジン(Chromium)などのモダンJSエンジンは、文字列を効率的に扱うために「スライス文字列(Sliced String)」や「コンカチネーション文字列(Cons String)」といった内部最適化を行っています。元の大きな文字列参照を保持し、ポインタをずらすだけでメモリのコピーコストを削減しているのです。
しかし、レガシーな `substr(index, length)` のような「長さを指定するアプローチ」は、エンジン側の最適化パスにおいて、しばしば不都合な挙動を引き起こします。特にマルチバイト文字(サロゲートペアや絵文字)が混在する現代の多言語Webアプリにおいて、文字数(Length)ベースの計算は、内部的なUTF-16コードユニットのインデックス計算とズレを生じさせやすく、予期せぬメモリの再割り当てやGC(ガベージコレクション)のスパイクを誘発する温床となります。
サロゲートペアという名の破壊神
例えば、絵文字を含む文字列を `substr` で切り取るとどうなるでしょうか?
const str = “👨💻Engineer”;
console.log(str.substr(0, 2)); // 「👨」の途中で切断され、化け文字や不正なサロゲートペアが誕生する
これは単なる表示崩れにとどまらず、バックエンドのAPIへ不正なJSON文字列を送信し、データベース層でUnicodeDecodeErrorを引き起こすといった、システム全体を巻き込む重大なバグ(セキュリティリスク)に直結します。
—
3. 現代のJSエンジニアが取るべき代替戦略
では、私たちはどのメソッドを使うべきなのでしょうか?
答えはシンプルです。`String.prototype.slice()` を使いましょう。
// レガシー
const token = authHeader.substr(7, 32);
// モダンかつ堅牢
const token = authHeader.slice(7, 39); // 7文字目から39文字目まで(終了インデックスを指定)
`slice()` は配列の `Array.prototype.slice()` とインターフェースが完全に統一されており、認知負荷が低く、JSエンジンの最適化も最も効きやすい形に設計されています。
より複雑な文字列操作にはテンプレートリテラルを
単なる切り出しだけでなく、動的な文字列構築やフォーマットには、もはや結合演算子(`+`)や非効率な関数群を使う必要はありません。テンプレートリテラル(Template Literals)を活用し、コードの可読性とパフォーマンスを極限まで高めましょう。
/
- 堅牢なユーザープロフィールのURL生成ユーティリティ
- @param {string} baseUrl – ベースURL
- @param {string} userId – ユーザーID(事前にサニタイズ済みを想定)
- @returns {string} フォーマット済みURL
/
const createUserProfileEndpoint = (baseUrl, userId) => {
// sliceを用いて安全にIDをプレフィックス化して切り出す
const secureIdHash = userId.trim().slice(0, 16);
// テンプレートリテラルによるタグ付き関数や、明示的な補間
return `${baseUrl}/v1/users/${secureIdHash}/profile`;
};
—
4. レガシーコードを駆逐する実務的なアプローチ
「理屈は分かった。だが、うちの数百万行ある巨大なコードベースから `substr` をどうやって見つけ出し、安全に置換すればいいんだ?」
ここからが、シニア・アーキテクチャの腕の見せ所です。人力のコードレビューや、勘に頼ったリファクタリングは今すぐやめましょう。機械的かつ絶対的な防衛ラインを構築します。
1. 静的解析(ESLint)による完全ブロック
プロジェクトの `eslintrc` に以下のルールを組み込み、CI/CDパイプラインで `substr` の存在を物理的にコンパイルエラー(またはビルドエラー)にします。
{
“rules”: {
“no-restricted-syntax”: [
“error”,
{
“selector”: “CallExpression[callee.property.name=’substr’]”,
“message”: “String.prototype.substr() は非推奨です。String.prototype.slice() を使用してください。”
}
]
}
}
この設定を入れておけば、未来のチームメンバー(あるいは未来の自分自身)がうっかり `substr` を書いた瞬間、PRのビルドが落ちるようになります。人間を信用するな、システムを信用しろ――これがアーキテクトの鉄則です。
2. マイグレーションスクリプトの活用
もしコードベースに数千箇所の `substr` が散らばっているなら、`jscodeshift` などのAST(抽象構文木)変換ツールを使って、安全に一括置換するコードを書きましょう。手動で置換してデグレを生むリスクを冒す必要はありません。
—
まとめ:細部に宿るプロフェッショナリズム
たかが文字列の切り出し、されど文字列の切り出し。
`substr()` のようなレガシーなAPIを排除し、モダンで堅牢な `slice()` やテンプレートリテラルへ置き換えることは、単なる「古いコードの掃除」ではありません。
- ブラウザエンジンの最適化パスを阻害しないためのパフォーマンス配慮
- サロゲートペアや文字エンコーディングの罠を踏まないためのセキュリティ配慮
- ESLintやASTを用いた、チーム全体をスケールさせるための組織的エンジニアリング
これらすべてが、あなたの書くコードの「格」を決めます。
今日からあなたのプロジェクトでも、`substr` という文字を完全に駆逐し、より美しく、より堅牢なフロントエンド・アーキテクチャを築き上げてください。それでは、良きコーディングライフを!

コメント