フロントエンドの現場でコードレビューをしていると、文字列の切り出しごときに `substring` を使ったり、挙動をよく理解していないまま適当に引数を渡してバグを生んでいるシーンに遭遇することが結構ある。
文字列操作はフロントエンド開発の基本中の基本だが、`String.prototype.slice()` の仕様を本当に正確に理解し、裏側のメモリの動きまでイメージできている中級エンジニアは、実はそう多くない。
今回は、この `slice()` にスポットを当てて、負のインデックスの挙動からブラウザの裏側の話、そして実務で即座に使える実践的なテクニックまで、シニアの視点で徹底的に叩き込んでいこう。
—
1. なぜ今、改めて `slice()` なのか?
JavaScriptの文字列から一部を切り出すメソッドには、歴史的経緯もあり `substr()`、`substring()`、そして `slice()` の3つが存在する。
- `substr(start, length)`:非推奨(Annex Bのレガシー機能)。もう使うな。
- `substring(start, end)`:引数の大小を自動で入れ替える変な仕様があり、直感に反することが多い。
- `slice(beginIndex, endIndex)`:最も素直で、Arrayの `slice()` とも一貫性がある最強のメソッド。
フロントエンドでAPIから受け取ったISO日付をパースしたり、長すぎるUIのテキストを三点リーダー(`…`)で丸めたりする処理において、迷わず選ぶべきは `slice()` 一択だ。
—
2. `slice()` の基本仕様と「負のインデックス」の魔力
まずは基本の復習と、中級者が一瞬迷う「負のインデックス」の仕組みを整理する。
const str = “Hello, Frontend!”;
// 基本:インデックス0から5の前まで(5文字目を含まない)
console.log(str.slice(0, 5)); // “Hello”
// 第2引数を省略すると、文字列の最後まで切り出す
console.log(str.slice(7)); // “Frontend!”
ここまでは余裕だと思う。問題は、負のインデックス(マイナスの値)を指定したときだ。
負のインデックスの正体
`slice()` に負の値を渡すと、JavaScriptエンジンは 「文字列の長さ(`str.length`)+ 負のインデックス」 という計算を裏側で行う。
例えば、`str.length` が 16 の場合:
- `str.slice(-8)` は `str.slice(16 + (-8))` つまり `str.slice(8)` と等価になる。
const filename = “bundle.min.js”;
// 後ろから6文字分を取得する(”.min.js”)
console.log(filename.slice(-6)); // “.min.js”
// 開始位置に負、終了位置にも負を指定する高度な技
// 「後ろから9文字目」から「後ろから3文字目の前」まで
// slice(16 – 9, 16 – 3) => slice(7, 13)
console.log(filename.slice(-9, -3)); // “min”
この「後ろからの相対位置」をパッと頭の中で計算できると、ファイル名やパスの操作で無駄な `length` の引き算を書かずに済む。コードが圧倒的にスマートになるポイントだ。
—
3. ブラウザ(V8エンジンなど)の裏側の話
「文字列を切り出す」という処理、JavaScriptのエンジン(ChromeのV8など)の内部ではどう処理されているだろうか?
「新しい文字列メモリを毎回ゴリゴリ確保してコピーしているのでは?」と心配するパフォーマンス厨のエンジニアもいるかもしれないが、現代のJSエンジンはそんなに愚かではない。
多くのモダンなJSエンジンでは、大きな文字列から `slice()` で部分的に切り出した場合、元の文字列のメモリ領域(バッファ)を共有し、単に「開始位置(offset)」と「長さ(length)」のメタデータだけを新しい文字列オブジェクトに持たせる(Rope文字列やスライス文字列としての最適化) ことが行われる。
つまり、`slice()` は比較的軽量な操作だ。
ただし、極端に巨大な数MBのHTML文字列などから、ごく一部を切り出して長期保持するようなケースでは、内部的に元の巨大なメモリ参照が残ることでガベージコレクション(GC)の邪魔になるリスクがゼロではない。SPAのメモリリークを気にする現場であれば、この「メモリ共有の特性」は頭の片隅に置いておいて損はない。
—
4. 実務で即コピペできる!実践的スニペット集
理屈はこれくらいにして、現場でそのまま使える実用的なパターンをいくつか紹介しよう。
パターンA:長すぎるタイトルの省略表示(三点リーダー付与)
UIコンポーネントでよくある、一定文字数を超えたら切り詰めて `…` をつける処理。
/
- 文字列を指定文字数で切り詰め、超過した場合は指定のサフィックスを付与する
- @param {string} text – 対象の文字列
- @param {number} maxLength – 最大文字数
- @param {string} [suffix=’…’] – 省略記号
- @returns {string}
/
const truncateText = (text, maxLength, suffix = ‘…’) => {
if (typeof text !== ‘string’) return ”;
// 文字数が上限以下ならそのまま返す
if (text.length <= maxLength) {
return text;
}
// sliceで切り詰めてサフィックスを結合
return text.slice(0, maxLength) + suffix;
};
// 使用例
console.log(truncateText("TypeScriptの高度な型推論について", 10));
// 出力: "TypeScript…"
パターンB:ファイル拡張子の安全な抽出
ユーザーがアップロードしたファイルのバリデーションなどで、拡張子を小文字で取得したいとき。
/
- ファイル名から拡張子をドットを除いた状態で小文字で取得する
- @param {string} filename
- @returns {string}
/
const getFileExtension = (filename) => {
if (!filename || !filename.includes(‘.’)) return ”;
// 最後のドットの位置を見つけて、その次から最後までをスライス
const lastDotIndex = filename.lastIndexOf(‘.’);
return filename.slice(lastDotIndex + 1).toLowerCase();
};
// 使用例
console.log(getFileExtension(“Avatar.Image.PNG”)); // “png”
console.log(getFileExtension(“README”)); // “”
—
5. シニアからのアドバイス:エラーハンドリングと型安全
最後に、実務で絶対にやらかしてほしくない注意点を一つ。
JavaScriptの `String.prototype.slice()` は非常に寛容だ。第一引数や第二引数にわけのわからない値(`null` や `undefined`、オブジェクトなど)を突っ込んでも、内部で暗黙の型変換(Type Coercion)が行われてクラッシュしにくい。
だが、それに甘えてはならない。
TypeScriptを使っていれば型で弾かれるが、レガシーなJS混じりのコードベースや、APIから型定義なしで `any` や `unknown` でデータが流れ込んでくるフロントエンドの境界線(Boundary)では、予期せぬ型が渡ってきて `TypeError: str.slice is not a function` を吐くことがある。
// 危険なコード:APIから返ってきたデータがnullの可能性がある
function renderCard(data) {
// data.title が null だったら即死する
const shortTitle = data.title.slice(0, 20);
}
// 堅牢なコード
function renderCardSafe(data) {
// オプショナルチェイニングと、文字列への明示的なキャスト(あるいはガード)
const title = typeof data?.title === ‘string’ ? data.title : ”;
const shortTitle = title.slice(0, 20);
}
フロントエンドエンジニアの仕事は、動くコードを書くだけではなく、「データが汚染されていてもアプリ全体を絶対にクラッシュさせない要塞のようなコード」を書くことだ。
`slice()` のような基本的なメソッドほど、その仕様の裏側まで理解し、丁寧なガード句と組み合わせて使いこなしてほしい。あなたの書くコードの質が、チーム全体の信頼に直結する。

コメント