【テクニカル・上級編】 String.prototype.substring()との比較 – JavaScript実践ガイド

なぜ、私たちはまだ `substring()` の挙動で消耗しているのか

フロントエンドのアーキテクチャ設計において、文字列操作は最もプリミティブでありながら、最も見落とされがちなボトルネックの一つだ。DOMの構築、仮想DOMの差分検出、APIペイロードのサニタイジング、そして巨大なログデータのストリーム処理に至るまで、文字列を切り出す処理はアプリの至るところで毎秒何千回も実行されている。

そこで問いたい。あなたは本当に `String.prototype.slice()` と `String.prototype.substring()` の違いを理解し、ユースケースに応じて完璧に使い分けているだろうか?
「なんとなく文字列を切り出すだけなら一緒だろ」と思ってコードレビューをスルーしていると、ある日突然、負の引数を渡した瞬間に予期せぬ巨大な文字列が返ってきたり、V8エンジンのガベージコレクションを無駄に刺激してメモリプレッシャーを高めたりするバグを踏み抜くことになる。

今回は、JavaScriptの文字列操作メソッドの中でも、特に歴史的経緯とブラウザの互換性の闇を背負った `substring()` にスポットを当て、`slice()` との決定的な違いをエンジニアリングの観点から丸裸にする。

—

`slice()` と `substring()` の根本的な仕様の乖離

まずは、両者のインターフェースと、内部的な引数の解釈における「決定的な思想の違い」を整理しておこう。

| 特徴 | `String.prototype.slice()` | `String.prototype.substring()` |
| :— | :— | :— |
| 負の引数の扱い | 文字列の末尾からの相対インデックスとして解釈する | `0` として扱う(または無視される) |
| 引数の大小関係 | `start > end` の場合、空文字 `””` を返す | 自動的にスワップ(入れ替え)する |
| 仕様の起源 | Arrayの `slice` との統一性を重視したモダンな設計 | JavaScript初期のNetscape時代からのレガシーな仕様 |

この表を見ただけで、`substring()` がいかに「お節介」で、モダンなAPI設計の文脈から外れているかが分かるだろう。しかし、この「お節介」な挙動こそが、プロダクションコードにおいて恐ろしいバグの温床となる。

1. 負の引数に対する挙動の罠

アプリケーション層で、例えば「ファイル名から拡張子のドットより前を切り出す」あるいは「URLの末尾から特定文字数を削る」といった処理を書くとき、私たちはよく負のインデックスを使いたくなる。

// パフォーマンスと安全性を意識したユーティリティのつもりだったが…
const getSafeString = (str, start, end) => {
// sliceなら負の数で「後ろからN文字目」を指定できる
return str.slice(start, end);
};

const filename = “bundle.min.js”;

// 例1: 正常系 (sliceもsubstringも同じ結果)
console.log(filename.slice(0, 6)); // “bundle”
console.log(filename.substring(0, 6)); // “bundle”

// 例2: 負の引数を用いた場合
console.log(filename.slice(-7)); // “min.js” (末尾から7文字)
console.log(filename.substring(-7)); // “bundle.min.js” (-7が0に丸め込まれ、substring(0)と同じになる)

`substring(-7)` はエラーを吐くわけでもなく、しれっと `substring(0)` として評価され、結果的に文字列全体のコピーが返ってくる。もしこれが数MBに及ぶ巨大なJSON文字列やテキストデータだったらどうなるか? 意図しないメモリ割り当て(Allocation)が発生し、メインスレッドをブロックする原因になりかねない。

2. 引数の順序ミスに対する「過度な優しさ」

もう一つの特徴が、`start > end` となった場合の挙動だ。

const text = “JavaScript”;

// sliceの場合:開始が終了より後ろなので、切り出せる範囲がなく空文字を返す
console.log(text.slice(5, 2)); // “”

// substringの場合:引数の大きい方と小さい方を勝度に入れ替えて実行する
console.log(text.substring(5, 2)); // text.substring(2, 5) と解釈され “vaSc” が返る

……いや、待ってほしい。バグで引数の順序が逆になってしまったときに、勝手に解釈してよしなに動いてくれることが、本当に「堅牢なシステム」と言えるだろうか?
答えは明確にノーだ。フェイルファスト(Fail-fast)の原則に反し、誤った引数をサイレントに補正して実行を続けるコードは、デバッグを困難にし、ロジックの破綻を隠蔽する最悪のアンチパターンである。

—

メモリ効率とV8エンジンの内部挙動の観点

ここで少しレイヤーを下げて、JavaScriptエンジン(ここではV8を想定)が文字列をメモリ上でどのように扱っているかを見てみよう。

V8において、文字列は通常、ヒープ上に配置される。しかし、`slice()` や `substring()` を用いて部分文字列(Substring)を生成する際、エンジンは必ずしも新しいメモリ領域を確保して文字列の文字を全てコピーするわけではない。
一定以上の長さを持つ文字列に対しては、元の文字列のバッファ(Backing Store)へのポインタと、オフセット・長さを保持する軽量な構造体(SlicedString)として最適化されることがある。

しかし、この最適化は万能ではない。
特に `substring()` が持つ「引数のスワップ」や「負の数の丸め込み」といった暗黙の型・値変換のオーバーヘッドは、V8のインラインキャッシュ(IC)の効率に微妙な影を落とす。高速なJITコンパイルの恩恵を受けるためには、関数に渡される引数の型の揺らぎや、条件分岐の予測可能性を極限まで高める必要がある。

`substring()` のように「引数の順序によって内部の挙動が動的に変わる」APIは、V8の最適化パイプラインにおいてモルフォロジカルな複雑さを増大させ、メガモフィック(Megamorphic)な状態を誘発するリスクすら孕んでいるのだ。

—

実務におけるアーキテクチャ上の選択指針

では、私たちは今後 `substring()` を一切使わず、すべて `slice()` に置き換えるべきなのだろうか?
結論から言えば、新規に記述するコードにおいて `substring()` を選択する正当な理由はほぼ存在しない。 配列(Array)やTypedArrayが持つ `slice()` とのAPIの一貫性を保つためにも、文字列操作も `slice()` に統一するのがモダンなフロントエンド・アーキテクチャの標準プラクティスである。

しかし、レガシーなコードベースやサードパーティの型定義を扱う上では、以下のような防御的プログラミングの意識が求められる。

実践:堅牢な文字列切り出しラッパーの設計

もし、入力値が外部から動的に渡され、負の数や順序の逆転が起こり得るカオスな環境で安全に文字列を処理したい場合は、以下のような型安全なラッパー関数を定義し、処理の境界(Boundary)でバリデーションを行うべきだ。

/

  • 予期せぬ引数の揺らぎを許容せず、安全に文字列を切り出す高堅牢性ラッパー

/
export function safeSlice(
target: string,
start: number,
end?: number
): string {
// 意図しない負の数や NaN が混入していないかを厳格にチェック
if (typeof target !== ‘string’) {
throw new TypeError(‘safeSlice expects a string as the first argument.’);
}

// 負の数や引数の順序逆転を許可しない厳密な設計にする場合
if (start < 0 || (end !== undefined && end < 0)) { throw new RangeError('Negative indices are not allowed in safeSlice.'); } if (end !== undefined && start > end) {
throw new RangeError(‘The start index cannot be greater than the end index.’);
}

return target.slice(start, end);
}

このようなラッパーを一枚噛ませるだけで、`substring()` が引き起こす「気づかないうちにデータが破壊される、あるいは意図せぬ巨大データが生成される」という不気味なバグを完全にコンパイル時・実行時でシャットアウトできる。

—

まとめ:道具の歴史を知り、モダンな選択を

`String.prototype.substring()` は、JavaScriptの黎明期を支えた歴史的な遺物である。その優しげな(しかし実態はお節介な)仕様は、当時の緩いWebの空気を反映したものに他ならない。

しかし、数万行規模のコードベースを動き、複雑な状態管理と高速なレンダリングが求められる現代のWebアプリケーションにおいて、その「暗黙の補正」は牙をむく。
今すぐプロジェクト内のコードベースを検索し、`substring()` が使われている箇所を洗い出してみてほしい。そして、その場所で本当にその挙動が必要なのかを疑い、一貫性と predictability(予測可能性)の高い `slice()` へとリファクタリングを試みてほしい。

細部へのこだわり、それこそが「動くコード」と「プロダクション・グレードの堅牢なアーキテクチャ」を分ける唯一の境界線なのだから。

コメント

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