ウェブエンジニア問題集
第8章

文字列操作と正規表現 — replace・split・slice・正規表現の実践ガイド

20
この章の目次開く

フォームの入力値を整える、URLを組み立てる、APIから来たテキストを加工する——文字列操作は配列と並んで毎日触る処理です。

この章では、文字列の「メソッド」とは何かという前提から始めて、sliceincludesreplace/replaceAllsplit など実務で頻出するメソッドと、テンプレートリテラル・正規表現の基本を用途別に整理します。

学習者学習者

文字列のメソッドって数が多くて、毎回MDNを開いて調べてる…。よく使うものだけ整理してほしい。

この章は、その「よく使うもの」を用途別にまとめた早見表のように使えます。配列操作で迷ったときは、配列メソッド早見表も合わせて確認してください。


そもそも文字列メソッドとは — String.prototype のメソッド

'hello'.toUpperCase() のように、文字列リテラルに対して直接メソッドを呼べるのはなぜでしょうか。文字列は本来オブジェクトではなくプリミティブ値なのに、です。

const s = 'hello';
console.log(typeof s); // "string"(オブジェクトではない)
s.toUpperCase(); // なのにメソッドが呼べる
js

これは、メソッドを呼んだ瞬間にJavaScriptが文字列を一時的に String オブジェクトに包む(自動ボックス化) からです。メソッドは String.prototype に定義されており、そこから借りて実行されます。配列が Array.prototype からメソッドを借りるのと同じ仕組みです。

console.log(String.prototype.hasOwnProperty('toUpperCase')); // true
js
先生先生

だから「文字列メソッド」=「String.prototype のメソッド」。配列のときと同じ考え方だね。

文字列を加工して新しい文字列を作るイメージ

文字列はイミュータブル — すべてのメソッドは「新しい文字列」を返す

最重要の前提です。文字列は一度作ると中身を変更できません(イミュータブル)。 そのため、replacetoUpperCase などのメソッドは元の文字列を変えず、加工した新しい文字列を返します

const original = 'hello';
const upper = original.toUpperCase();
 
console.log(upper); // "HELLO"(新しい文字列)
console.log(original); // "hello"(元はそのまま)
js
学習者学習者

text.replace(...) を呼んだのに元の変数が変わってない…!バグかと思った。

バグではありません。戻り値を受け取らないと結果は捨てられます。const result = text.replace(...) のように必ず戻り値を使うのが鉄則です。

テンプレートリテラル — 変数を埋め込む

バッククォート(`)で囲むと、${...} の中に変数や式を埋め込めます。文字列の + 連結より圧倒的に読みやすく、現在の標準です。

const name = '太郎';
const age = 25;
 
// ❌ + 連結は読みにくい
const a = 'こんにちは、' + name + 'さん(' + age + '歳)';
 
// ⭕ テンプレートリテラル
const b = `こんにちは、${name}さん(${age}歳)`;
 
// 改行もそのまま書ける
const html = `
  <p>${name}</p>
  <p>${age}</p>
`;
js

抽出 — slice / substring / at

文字列の一部を切り出します。実務では slice を覚えておけばほぼ事足ります

構文: str.slice(start, end?)

引数説明
start切り出しを始める位置(0始まり)。マイナスを指定すると末尾から数える
end(省略可)切り出しを終える位置。その手前までが対象で end 自身は含まない。省略すると末尾まで

戻り値: 切り出した新しい文字列(元の文字列は変わらない)

at は1文字を取り出すメソッドで、構文は str.at(index)index にマイナスを指定すると末尾から数えられます(-1 で最後の文字)。

const str = 'JavaScript';
 
str.slice(0, 4); // "Java"(0番目から4番目の手前まで)
str.slice(4); // "Script"(4番目から最後まで)
str.slice(-6); // "Script"(末尾から6文字。マイナス指定が便利)
 
str.at(0); // "J"(最初の文字)
str.at(-1); // "t"(最後の文字。str[str.length - 1] より簡潔)
js

検索 — includes / indexOf / startsWith / endsWith

文字列が含まれているか・どこにあるかを調べます。いずれも「探したい文字列」を引数に渡すだけです。

メソッド引数戻り値
str.includes(searchString)searchString:探す文字列boolean(含まれるか)
str.indexOf(searchString)searchString:探す文字列最初に現れた位置(数値)。見つからなければ -1
str.startsWith(searchString)searchString:探す文字列boolean(前方一致するか)
str.endsWith(searchString)searchString:探す文字列boolean(後方一致するか)
const url = 'https://example.com/users/1';
 
url.includes('users'); // true(含まれるか → boolean)
url.startsWith('https'); // true(前方一致)
url.endsWith('.com'); // false
url.indexOf('users'); // 20(位置。見つからなければ -1)
js

置換 — replace / replaceAll

文字列を置き換えます。実務で非常によく使う一方、ハマりやすい挙動があるので丁寧に見ます。

構文: str.replace(pattern, replacement)

引数渡せるもの説明
pattern(第1引数)文字列 / 正規表現何を置き換えるか。文字列を渡すと最初にマッチした1件だけが対象になる(要注意)
正規表現を渡すとマッチしたすべての部分が対象になる
replacement(第2引数)文字列 / 関数何に置き換えるか。文字列の中で $1$2… で正規表現のキャプチャ(後述)を参照でき、関数を渡すとマッチした値から動的に計算できる

戻り値: 置換後の新しい文字列(元の文字列は変わらない)

replaceAll(pattern, replacement) も引数の意味は同じで、違いはマッチしたすべてを置き換える点だけです。

この2つの引数(patternreplacement)にそれぞれ何を渡せるかで挙動が変わります。パターン別に見ていきましょう。

基本(文字列で指定すると「最初の1件」だけ)

const text = 'りんご, りんご, みかん';
 
text.replace('りんご', 'ぶどう');
// "ぶどう, りんご, みかん" ← 最初の1件だけ置換される!
js
学習者学習者

全部置き換えたいのに、1個目しか変わらない…!なんで?

replace文字列を渡すと、置換されるのは最初にマッチした1件だけです。これは初心者がほぼ必ず引っかかるポイントです。すべて置換するには次の2つの方法があります。

すべて置換する — replaceAll(推奨)

const text = 'りんご, りんご, みかん';
 
text.replaceAll('りんご', 'ぶどう');
// "ぶどう, ぶどう, みかん" ← 全部置換される
js

replaceAll(ES2021)が最もシンプルで読みやすい方法です。古い環境向けには、後述の正規表現に g(グローバル)フラグを付ける方法もあります。

// 正規表現 + g フラグでも全置換できる(replaceAll が使えない環境向け)
text.replace(/りんご/g, 'ぶどう');
js

正規表現で柔軟に置換 + キャプチャ($1)

第1引数に正規表現を渡すと、パターンマッチで置換できます。() でグループ化した部分は、置換文字列の中で $1$2… として参照できます。

// 2024-01-31 → 2024/01/31 に変換
'2024-01-31'.replace(/(\d{4})-(\d{2})-(\d{2})/, '$1/$2/$3');
// "2024/01/31"
js

コールバックで動的に置換

置換後の値を関数で計算することもできます。マッチした文字列が引数で渡されます。

// 各単語の先頭を大文字に
'hello world'.replace(/\b\w/g, (char) => char.toUpperCase());
// "Hello World"
js

大文字小文字・空白・繰り返し

'Hello'.toUpperCase(); // "HELLO"
'Hello'.toLowerCase(); // "hello"
 
'  hello  '.trim(); // "hello"(前後の空白を除去)
'  hello  '.trimStart(); // "hello  "
'5'.padStart(3, '0'); // "005"(左を0埋め。ゼロパディングの定番)
'5'.padEnd(3, '*'); // "5**"
js

padStart / padEnd の構文は str.padStart(targetLength, padString?) です。

引数説明
targetLength最終的に揃えたい文字数。元の文字列がすでにこの長さ以上なら何も埋めない
padString(省略可)埋めるのに使う文字。デフォルトは半角スペース

repeat — 文字列を指定回数繰り返す

構文: str.repeat(count)

引数説明
count繰り返す回数(0以上の整数。0なら空文字列)

戻り値: 元の文字列を count 回連結した新しい文字列

'abc'.repeat(3); // "abcabcabc"
'-'.repeat(40); // "----------------------------------------"(区切り線を作る)
'⭐'.repeat(5); // "⭐⭐⭐⭐⭐"(レーティング表示など)
js

実務では区切り線の生成やインデント文字列の構築に使います。

function indent(level) {
  return '  '.repeat(level);
}
indent(3) + 'nested'; // "      nested"
js

分割と結合 — split / join

文字列を配列に分割(split)し、配列を文字列に結合(join)します。配列処理との橋渡しになる重要メソッドです。

メソッド引数戻り値
str.split(separator, limit?)separator:区切り文字('' を渡すと1文字ずつ) / limit(省略可):取り出す最大要素数配列
arr.join(separator?)separator(省略可):要素をつなぐ文字(省略すると ,文字列
// 分割:文字列 → 配列
'a,b,c'.split(','); // ["a", "b", "c"]
'hello'.split(''); // ["h", "e", "l", "l", "o"](1文字ずつ)
 
// 結合:配列 → 文字列
['a', 'b', 'c'].join('-'); // "a-b-c"
 
// 組み合わせ:CSVの各値をトリムする
'  a , b , c '
  .split(',')
  .map((s) => s.trim())
  .join(','); // "a,b,c"
js

正規表現の基本

正規表現(RegExp)は「文字列のパターン」を表す仕組みです。replace や検索と組み合わせて使います。

test — マッチするかだけ調べる

構文: regex.test(str)

true / false を返すだけのシンプルなメソッド。バリデーションに最適です。

const re = /\d{3}-\d{4}/; // 「数字3桁-数字4桁」のパターン(郵便番号など)
 
re.test('123-4567'); // true
re.test('abc-defg'); // false
 
// 実務パターン: メールアドレスの簡易チェック
const emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
emailRe.test('user@example.com'); // true
js

match — 最初のマッチを詳しく取得する

構文: str.match(regex)

g フラグ戻り値
なし最初のマッチ1件 + キャプチャグループ + index 情報(配列)
ありマッチした部分文字列の配列(キャプチャ情報はなし)
マッチなしnull
// g フラグなし → 最初の1件を詳しく取得
const result = '2024-01-31 の次は 2024-02-01'.match(/(\d{4})-(\d{2})-(\d{2})/);
console.log(result[0]); // "2024-01-31"(マッチ全体)
console.log(result[1]); // "2024"(1番目のキャプチャ)
console.log(result[2]); // "01"
console.log(result.index); // 0(マッチ開始位置)
 
// g フラグあり → すべてのマッチを取り出す(ただしキャプチャは取れない)
'2024-01-31 の次は 2024-02-01'.match(/\d{4}-\d{2}-\d{2}/g);
// ["2024-01-31", "2024-02-01"]
js
学習者学習者

g ありだと全部取れるけどキャプチャが消える…全部のマッチでキャプチャも欲しいときは?

そこで matchAll の出番です。

matchAll — すべてのマッチを詳細に取得する

構文: str.matchAll(regex)g フラグ必須)

戻り値: 各マッチの詳細情報(キャプチャグループ・index 含む)を順に返すイテレータ

matchg フラグを付けると全マッチが取れますが、キャプチャグループの情報が失われます。matchAllすべてのマッチについて、キャプチャやインデックスも含めた詳細情報を取得できます。

const text = '価格: ¥1,200(税込¥1,320)';
const regex = /¥([\d,]+)/g;
 
// matchAll はイテレータを返すのでスプレッドか for...of で展開する
const matches = [...text.matchAll(regex)];
 
console.log(matches[0][0]); // "¥1,200"(マッチ全体)
console.log(matches[0][1]); // "1,200"(キャプチャグループ)
console.log(matches[0].index); // 4(マッチ開始位置)
 
console.log(matches[1][0]); // "¥1,320"
console.log(matches[1][1]); // "1,320"
js

実務パターン: HTMLからリンクを抽出

const html = '<a href="/about">About</a> と <a href="/contact">Contact</a>';
const linkRe = /<a href="([^"]+)">([^<]+)<\/a>/g;
 
for (const m of html.matchAll(linkRe)) {
  console.log(`URL: ${m[1]}, テキスト: ${m[2]}`);
}
// URL: /about, テキスト: About
// URL: /contact, テキスト: Contact
js

実務パターン: ログからデータを構造化

const log = '[2024-01-15 09:30] ERROR db connection failed\n[2024-01-15 09:31] INFO retry succeeded';
const logRe = /\[(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})\] (\w+) (.+)/g;
 
const entries = [...log.matchAll(logRe)].map((m) => ({
  date: m[1],
  time: m[2],
  level: m[3],
  message: m[4],
}));
// [{ date: "2024-01-15", time: "09:30", level: "ERROR", message: "db connection failed" }, ...]
js

名前付きキャプチャグループ — (?<name>...)

ES2018で追加された機能で、キャプチャグループに名前を付けられます。$1$2 のような番号よりコードの意図が明確になります。

const dateStr = '2024-01-31';
const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
 
const { groups } = dateStr.match(re);
console.log(groups.year); // "2024"
console.log(groups.month); // "01"
console.log(groups.day); // "31"
js

matchAll との組み合わせが特に強力です。

const text = '田中太郎 (30歳), 佐藤花子 (25歳)';
const re = /(?<name>\S+)\s*\((?<age>\d+)歳\)/g;
 
const people = [...text.matchAll(re)].map((m) => ({
  name: m.groups.name,
  age: Number(m.groups.age),
}));
// [{ name: "田中太郎", age: 30 }, { name: "佐藤花子", age: 25 }]
js

replace でも $<name> で参照できます。

'2024-01-31'.replace(
  /(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/,
  '$<y>年$<m>月$<d>日',
);
// "2024年01月31日"
js

よく使うパターン部品

記法意味
\d数字1文字(\D は数字以外)
\w英数字・アンダースコア
\s空白文字
.任意の1文字
+直前を1回以上
*直前を0回以上
?直前を0回または1回
{n}直前をn回
{n,m}直前をn回以上m回以下
^ / $行頭 / 行末
[abc]a,b,cのいずれか
[^abc]a,b,c以外
|または
(?:...)キャプチャしないグループ

主なフラグ:g(全マッチ)、i(大文字小文字を無視)、m(複数行)、s. が改行にもマッチ)。

正規表現の全体像は MDN — 正規表現 にまとまっています。

メソッド早見表

やりたいことメソッド戻り値
一部を切り出すslice新しい文字列
含まれるか調べるincludesboolean
前方/後方一致startsWith / endsWithboolean
最初の1件を置換replace新しい文字列
すべて置換replaceAll新しい文字列
大文字/小文字化toUpperCase / toLowerCase新しい文字列
前後の空白除去trim新しい文字列
文字列を繰り返すrepeat新しい文字列
区切って配列にsplit配列
パターンに一致するか正規表現 + testboolean
最初のマッチを詳しくmatchg なし)配列 or null
全マッチ+キャプチャmatchAllg 必須)イテレータ

各メソッドの詳細は MDN — String リファレンス を参照してください。

よくあるハマりどころ

文字列操作のハマりどころを確認するイメージ

1. replace が最初の1件しか置換しない

前述のとおり、文字列指定の replace は最初の1件だけ。全置換は replaceAll(または /.../g)を使います。

2. 戻り値を使い忘れる

文字列はイミュータブル。text.trim() だけでは何も起きません。text = text.trim() のように戻り値を代入します。

3. length と「見た目の文字数」がずれる

絵文字や一部の文字は、length 上は2以上としてカウントされます。

'👨‍👩‍👧'.length; // 8(見た目は1文字なのに)
js

正確な「文字単位」で数えたい場合は [...str].length(スプレッドで分割)や Intl.Segmenter を使います。

ちゃんと使うためのポイント

  • 文字列メソッドは String.prototype のメソッド。文字列はイミュータブルで、メソッドは新しい文字列を返す(戻り値を必ず使う)
  • 連結はテンプレートリテラル(`${x}`)、抽出は slice、含有判定は includes が基本
  • 全置換は replaceAllreplace に文字列を渡すと最初の1件だけなので注意
  • 入力値は trim() で前後空白を落としてから扱う
  • 複雑なパターンの検索・置換は正規表現と組み合わせる
  • 全マッチ+キャプチャが必要なら matchAll。名前付きキャプチャ (?<name>...) で可読性を上げる

次の章では、分割代入とスプレッド構文 を扱います。split で作った配列の受け取りなど、ここで学んだ文字列操作とも自然につながります。

参考リンク

JavaScriptクイズに挑戦するこの章で学んだJavaScriptの知識を、4択クイズでアウトプットして定着させよう