やあ、お疲れ様。
最近、コードレビューをしていて「またここで罠を踏み抜いてるな…」って思うポイントがあるんだ。それが今回話す `String.prototype.substring()` と `String.prototype.slice()` の違い。
中級への階段を登っている君なら、文字列から一部分を切り出すときに `substring` や `slice` を当たり前のように使っていることだろう。でも、「なんとなく動くから」で使い分けていると、ふとした瞬間にバグの温床を踏むことになる。特に、APIから飛んできた生データや、レガシーなコードベースをリファクタリングしているときにね。
今回は、シニアの視点から、この `substring()` の正体と、モダンフロントエンド開発におけるベストプラクティスを叩き込んでおこう。
—
そもそも `substring()` とは何か?
`substring()` は、文字列の指定した開始インデックスから終了インデックスまでの部分文字列を返すビルトインメソッドだ。
一見すると `slice()` と同じように見える。実際、引数を2つとも正の整数で渡す分には、ほとんど同じ挙動をする。しかし、「負の数(マイナス)」を渡したとき、あるいは「開始位置が終了位置より大きい(大小関係が逆転している)」とき、エンジニアの意図を裏切る挙動を見せる。
ここが、現場で事故が起きる最大のポイントなんだ。
—
`slice()` との決定的な違い
まずは、JavaScriptのエンジンが裏側でどう処理しているのか、その違いを生々しく比較してみよう。
1. 負の引数(マイナス値)の扱い
ここが一番の急所だ。
- `slice()`: 負の値を「文字列の末尾からの相対位置」として解釈する。例えば `slice(-3)` なら「後ろから3文字目以降」を指す。非常に直感的で、実務でもめちゃくちゃよく使う。
- `substring()`: 負の値をすべて `0`(先頭) として扱う。さらに、もし第1引数が第2引数より大きい場合、勝手に引数をスワップ(入れ替え)しやがる。
言葉だけだとピンとこないだろ? サンプルコードを見てほしい。
// 検証用のサンプル文字列
const targetString = ‘FrontendExpert’;
// — ケース1: slice() の場合 —
// 後ろから6文字を切り出す(実務でよくあるユースケース)
console.log(targetString.slice(-6)); // 出力: “Expert”
// — ケース2: substring() の場合 —
// 負の数を渡すと、勝手に 0 に変換される
console.log(targetString.substring(-6)); // 出力: “FrontendExpert” (すべて返される)
どうだい? `substring(-6)` を書いたプログラマーは「後ろから6文字」欲しかったはずなのに、実際には文字列全体が返ってくる。これがテスト漏れして本番環境にデプロイされたときの絶望感と言ったら……想像に難くないだろう?
2. 引数の大小関係が逆転したとき
もう一つの罠がこれだ。開始インデックスに大きな数字、終了インデックスに小さな数字を渡した場合の挙動。
const text = ‘JavaScript’;
// slice() の場合:終了位置が開始位置より前にあるので、空文字が返る
console.log(text.slice(6, 2)); // 出力: “”
// substring() の場合:引数の大小関係を勝手に判定し、小さい方を開始、大きい方を終了に入れ替える!
// つまり text.substring(2, 6) と同じ扱いにされる
console.log(text.substring(6, 2)); // 出力: “Scri” (index 2 から 6 の文字が返る)
親切心のつもりなのかもしれないが、コーディングにおいて「勝手にいい感じに解釈してくれる仕様」は、大抵の場合バグの元凶になる。バグの追跡調査をしているときに、「えっ、ここで引数がスワップされてたの!?」と気づいたときの脱力感はすごいぞ。
—
なぜ `substring()` はこんな仕様なのか?(歴史的背景)
ちょっとしたトリビアだが、`substring()` は JavaScript の黎明期、文字通り言語の仕様が固まる前の大昔(Netscape Navigator の時代)から存在する由緒あるメソッドだ。そのため、当時のレガシーな仕様を引きずっている。
一方、後から追加された `slice()` は、配列(`Array.prototype.slice`)などでも使われる一貫性のある設計になっており、負のインデックスによる直感的な末尾指定をサポートしている。
フロントエンドの現場における事実上の標準(デファクトスタンダード)として、モダンなコードベースでは基本的に `slice()` を第一選択 にすべき理由がここにある。
—
現場で使える!実践的リファクタリングTips
じゃあ、実務でどうコードを書くべきか。具体的なコードパターンを見ていこう。
NGパターン:古い知識や思い込みで `substring` を使っている例
// 【NG】ファイル名から拡張子を剥がそうとして、うっかり負の数やバグを埋め込む例
function getBaseNameNG(filename) {
const lastDotIndex = filename.lastIndexOf(‘.’);
//もしドットが見つからず -1 が返ってきたら、substring(-1) は 0 になり、
//予期せぬ挙動を引き起こす!
return filename.substring(0, lastDotIndex);
}
GOODパターン:`slice` とテンプレートリテラル、モダンなAPIで安全に書く
/
- 安全にファイル名から拡張子を除去する関数
- @param {string} filename
- @returns {string}
/
function getBaseNameSafe(filename) {
const lastDotIndex = filename.lastIndexOf(‘.’);
// ドットが存在しない場合はそのまま返す防衛的プログラミング
if (lastDotIndex === -1) return filename;
// slice なら直感的に先頭からドットの直前までを安全に切り出せる
return filename.slice(0, lastDotIndex);
}
// 応用:文字列の末尾から特定文字を安全に取り除く(sliceの真骨頂)
function getExtension(filename) {
const lastDotIndex = filename.lastIndexOf(‘.’);
if (lastDotIndex === -1) return ”;
// 負のインデックスを使って後ろからスパッと切り出す
return filename.slice(lastDotIndex + 1);
}
const sampleFile = ‘bundle.min.js’;
console.log(getBaseNameSafe(sampleFile)); // 出力: “bundle.min”
console.log(getExtension(sampleFile)); // 出力: “js”
—
まとめ:シニアからの提言
JavaScript の世界には、歴史的経緯から「似たようなことができるけれど、片方は地雷原」という機能がいくつか存在する。`substring()` と `slice()` もその代表例だ。
- `substring()`: 基本的には使わない。レガシーコードの保守で出会う以外は、新規コードで選ぶ理由がほぼない。
- `slice()`: 文字列操作のファーストチョイス。負の数も扱えるし、挙動がロジカルで予測しやすい。
チームメンバーから「どっちを使えばいいですか?」と聞かれたら、胸を張って 「迷ったら `slice` を使え。負の数が使えるし、余計な忖度(勝手なスワップ)をしないから安全だ」 とアドバイスしてやってほしい。
こういう細かい仕様の解像度を上げていくことが、君をワンランク上のフロントエンド・エンジニアへと押し上げてくれるはずだ。さあ、今日のコードから早速 `slice` に置き換えていこうぜ!

コメント