【JavaScript】文字列内の特定文字の出現回数をカウントする方法|split・match・reduce完全ガイド

JavaScriptで文字列内の特定文字の出現回数をカウントするには、split().length - 1match()forループ・reduce()の4つのパターンがあります。

この記事では、それぞれの方法の使い方と違いを比較し、大文字小文字の無視部分文字列のカウント出現頻度マップの作成パフォーマンス比較実務パターンまで体系的に解説します。

この記事で学べること

  • split()match()forreduce() による4つのカウント方法
  • 大文字小文字を無視したカウント(i フラグ)
  • 部分文字列(複数文字)のカウント方法
  • 全文字の出現頻度マップの作成と最頻出・最少文字の取得
  • 各方法のパフォーマンス比較と使い分け
  • バリデーション・テキスト分析など実務での活用パターン
スポンサーリンク

split().length – 1 パターン

最もシンプルな方法は、対象文字で文字列を分割し、配列の長さから1を引くパターンです。分割すると対象文字の数より配列の要素が1つ多くなる性質を利用します。

基本構文

JavaScript
const str = 'Hello, World!';
const target = 'l';

// split で分割 → 配列の長さ - 1 = 出現回数
const count = str.split(target).length - 1;

console.log(count);  // 3

実行結果

3

"Hello, World!""l" で分割すると ["He", "", "o, Wor", "d!"] の4要素になります。4 – 1 = 3 が出現回数です。

関数化した例

JavaScript
function countChar(str, char) {
  return str.split(char).length - 1;
}

console.log(countChar('banana', 'a'));  // 3
console.log(countChar('hello', 'z'));   // 0
console.log(countChar('aaa', 'a'));     // 3

実行結果

3
0
3

注意:split() は内部で配列を生成するため、非常に長い文字列ではメモリ効率が悪くなる場合があります。大量データには for ループが適しています。

match() + 正規表現パターン(gフラグ)

match() メソッドに g(グローバル)フラグ付きの正規表現を渡すと、マッチした全ての結果が配列で返されます。その配列の長さが出現回数です。

基本構文

JavaScript
const str = 'Hello, World!';

// g フラグで全てのマッチを取得
const matches = str.match(/l/g);

console.log(matches);         // ['l', 'l', 'l']
console.log(matches.length);  // 3

実行結果

["l", "l", "l"]
3

マッチなしの場合の安全な書き方

match() はマッチが見つからない場合に null を返します。そのまま .length を呼ぶとエラーになるため、null チェックが必要です。

JavaScript
function countCharRegex(str, char) {
  const regex = new RegExp(char, 'g');
  const matches = str.match(regex);
  return matches ? matches.length : 0;
}

console.log(countCharRegex('banana', 'a'));  // 3
console.log(countCharRegex('hello', 'z'));   // 0

実行結果

3
0

ポイント:match()null を返す場合に備え、Null合体演算子 ?? を使って (str.match(regex) ?? []).length と書くこともできます。

正規表現の特殊文字をエスケープする

動的に正規表現を作る場合、.* などの特殊文字はエスケープが必要です。

JavaScript
function escapeRegex(str) {
  return str.replace(/[.*+?^${}()|[]\]/g, '\$&');
}

function countCharSafe(str, char) {
  const escaped = escapeRegex(char);
  const regex = new RegExp(escaped, 'g');
  return (str.match(regex) ?? []).length;
}

// 「.」をカウント(特殊文字)
console.log(countCharSafe('a.b.c.d', '.'));  // 3

実行結果

3

for ループでのカウント

最もプリミティブですが、最もパフォーマンスが良い方法です。追加の配列やオブジェクトを生成しないため、メモリ効率にも優れています。

基本の for ループ

JavaScript
function countCharLoop(str, char) {
  let count = 0;
  for (let i = 0; i < str.length; i++) {
    if (str[i] === char) {
      count++;
    }
  }
  return count;
}

console.log(countCharLoop('programming', 'g'));  // 2
console.log(countCharLoop('programming', 'm'));  // 2
console.log(countCharLoop('programming', 'z'));  // 0

実行結果

2
2
0

for…of ループ

for...of を使うとインデックス管理が不要になり、コードがより読みやすくなります。

JavaScript
function countCharForOf(str, char) {
  let count = 0;
  for (const c of str) {
    if (c === char) count++;
  }
  return count;
}

console.log(countCharForOf('JavaScript', 'a'));  // 2

実行結果

2

ポイント:for...of はサロゲートペア文字(絵文字など)も正しく1文字として扱います。for ループ + str[i] ではサロゲートペアを正しく処理できない場合があります。

reduce() でのカウント

関数型プログラミングのスタイルで書きたい場合は、Array.from()reduce() を組み合わせる方法があります。

基本パターン

JavaScript
function countCharReduce(str, char) {
  return [...str].reduce((acc, c) => c === char ? acc + 1 : acc, 0);
}

console.log(countCharReduce('mississippi', 's'));  // 4
console.log(countCharReduce('mississippi', 'i'));  // 4
console.log(countCharReduce('mississippi', 'p'));  // 2

実行結果

4
4
2

filter() を使ったワンライナー

reduce() の代わりに filter() を使う方法もあります。マッチする文字だけを抽出してその長さを取ります。

JavaScript
const countChar = (str, char) =>
  [...str].filter(c => c === char).length;

console.log(countChar('abracadabra', 'a'));  // 5

実行結果

5

4つの方法の比較

ここまで紹介した4つの方法を比較表でまとめます。

方法 コード例 特徴 可読性
split().length – 1 str.split(ch).length - 1 最もシンプル ★★★
match() + 正規表現 (str.match(/x/g) ?? []).length 柔軟なパターンマッチ ★★☆
for ループ for (let i...) if (str[i]===ch) 最速・メモリ効率良 ★★☆
reduce() [...str].reduce(...) 関数型スタイル ★★☆

大文字小文字を無視したカウント(iフラグ)

大文字と小文字を区別せずにカウントしたい場合は、正規表現の i(case-insensitive)フラグ を使うか、あらかじめ文字列を統一してからカウントします。

match() + gi フラグ

JavaScript
const str = 'Hello World';

// gi フラグ: g(全検索)+ i(大文字小文字無視)
const count = (str.match(/l/gi) ?? []).length;
console.log(count);  // 3

// 大文字の「L」も含めてカウント
const str2 = 'JavaScript Learning Lab';
const count2 = (str2.match(/l/gi) ?? []).length;
console.log(count2);  // 3('L' + 'l' + 'L' = 計3回)

実行結果

3
3

toLowerCase() で統一する方法

JavaScript
function countCharIgnoreCase(str, char) {
  return str.toLowerCase().split(char.toLowerCase()).length - 1;
}

console.log(countCharIgnoreCase('AbCaBcAbC', 'a'));  // 3
console.log(countCharIgnoreCase('AbCaBcAbC', 'A'));  // 3

実行結果

3
3

部分文字列のカウント(複数文字の出現回数)

1文字ではなく複数文字の部分文字列(サブストリング)の出現回数をカウントする方法を紹介します。

split() で部分文字列をカウント

JavaScript
function countSubstring(str, sub) {
  if (sub.length === 0) return 0;
  return str.split(sub).length - 1;
}

console.log(countSubstring('abcabcabc', 'abc'));  // 3
console.log(countSubstring('aaaa', 'aa'));       // 2(重複なし)
console.log(countSubstring('hello world', 'or')); // 1

実行結果

3
2
1

indexOf() で重複を含むカウント

split() は重複しない出現回数を返します。"aaaa" 中の "aa" を重複ありでカウントしたい場合(= 3回)は、indexOf() をずらしながら検索します。

JavaScript
function countOverlapping(str, sub) {
  let count = 0;
  let pos = 0;
  while ((pos = str.indexOf(sub, pos)) !== -1) {
    count++;
    pos += 1;  // 1文字ずつずらして重複検出
  }
  return count;
}

console.log(countOverlapping('aaaa', 'aa'));     // 3(重複あり)
console.log(countOverlapping('abcabcabc', 'abc'));  // 3
console.log(countOverlapping('ababab', 'aba'));   // 2

実行結果

3
3
2

注意:split()indexOf() では重複部分文字列のカウント結果が異なります。"aaaa" 中の "aa"split() なら2回、重複ありの indexOf() なら3回です。用途に応じて使い分けてください。

全文字の出現頻度マップ作成

文字列内のすべての文字の出現回数を一度にカウントし、頻度マップ(オブジェクト)として取得する方法です。テキスト分析や統計処理の基本パターンとして活用できます。

for…of で頻度マップを作成

JavaScript
function charFrequencyMap(str) {
  const freq = {};
  for (const char of str) {
    freq[char] = (freq[char] || 0) + 1;
  }
  return freq;
}

const result = charFrequencyMap('banana');
console.log(result);

実行結果

{ b: 1, a: 3, n: 2 }

reduce() で頻度マップを作成

JavaScript
function charFrequencyReduce(str) {
  return [...str].reduce((freq, char) => {
    freq[char] = (freq[char] || 0) + 1;
    return freq;
  }, {});
}

const result = charFrequencyReduce('hello world');
console.log(result);

実行結果

{ h: 1, e: 1, l: 3, o: 2, " ": 1, w: 1, r: 1, d: 1 }

Map を使った頻度マップ

より正確な頻度マップが必要な場合は、Map を使う方法もあります。Map はキーの挿入順序を保持し、任意の値をキーにできます。

JavaScript
function charFrequencyWithMap(str) {
  const freq = new Map();
  for (const char of str) {
    freq.set(char, (freq.get(char) || 0) + 1);
  }
  return freq;
}

const map = charFrequencyWithMap('apple');
console.log(map.get('p'));  // 2
console.log(map.get('a'));  // 1

実行結果

2
1

最頻出文字・最少文字の取得

頻度マップを基に、最も多く出現する文字最も少ない文字を取得する方法です。

最頻出文字を取得

JavaScript
function getMostFrequent(str) {
  const freq = {};
  for (const char of str) {
    freq[char] = (freq[char] || 0) + 1;
  }

  let maxChar = '';
  let maxCount = 0;

  for (const [char, count] of Object.entries(freq)) {
    if (count > maxCount) {
      maxChar = char;
      maxCount = count;
    }
  }

  return { char: maxChar, count: maxCount };
}

console.log(getMostFrequent('abracadabra'));

実行結果

{ char: "a", count: 5 }

最頻出・最少を同時に取得

JavaScript
function getFrequencyExtremes(str) {
  const freq = {};
  for (const char of str) {
    if (char !== ' ') {  // 空白を除外
      freq[char] = (freq[char] || 0) + 1;
    }
  }

  const entries = Object.entries(freq);
  const sorted = entries.sort((a, b) => b[1] - a[1]);

  return {
    most:  { char: sorted[0][0], count: sorted[0][1] },
    least: { char: sorted.at(-1)[0], count: sorted.at(-1)[1] },
    all: sorted
  };
}

const result = getFrequencyExtremes('programming');
console.log('最頻出:', result.most);
console.log('最少:', result.least);
console.log('全頻度:', result.all);

実行結果

最頻出: { char: "g", count: 2 }
最少: { char: "p", count: 1 }
全頻度: [["g",2],["r",2],["m",2],["p",1],["o",1],["a",1],["i",1],["n",1]]

パフォーマンス比較

各方法のパフォーマンスを performance.now() で計測してみましょう。100万文字の文字列で1,000回実行した平均値を比較します。

ベンチマークコード

JavaScript
// テスト用の長い文字列を生成
const testStr = 'a'.repeat(500000) + 'b'.repeat(500000);
const iterations = 100;

function benchmark(name, fn) {
  const start = performance.now();
  for (let i = 0; i < iterations; i++) fn();
  const elapsed = performance.now() - start;
  console.log(`${name}: ${elapsed.toFixed(2)}ms`);
}

benchmark('split',   () => testStr.split('a').length - 1);
benchmark('match',   () => (testStr.match(/a/g) ?? []).length);
benchmark('for',     () => {
  let c = 0;
  for (let i = 0; i < testStr.length; i++) if (testStr[i] === 'a') c++;
});
benchmark('reduce',  () => [...testStr].reduce((a,c)=>c==='a'?a+1:a,0));

パフォーマンス結果(参考値)

方法 相対速度 メモリ使用 備考
for ループ ★★★ 最速 最小 追加の配列を作らない
match() + 正規表現 ★★☆ 速い マッチ配列を生成
split().length – 1 ★★☆ 速い 分割した配列全体を保持
reduce() ★☆☆ 遅い スプレッド演算子で配列化

ポイント:通常の文字列(数百〜数千文字)では差はほぼ感じません。パフォーマンスが重要になるのは、数十万文字以上の大量テキストを処理する場合です。迷ったら可読性を優先して split()match() を選びましょう。

実務パターン

出現回数のカウントは、バリデーションテキスト分析などの実務で頻繁に活用されます。

パターン1: メールアドレスの @ 個数チェック

JavaScript
function hasValidAtSign(email) {
  const atCount = email.split('@').length - 1;
  return atCount === 1;
}

console.log(hasValidAtSign('user@example.com'));   // true
console.log(hasValidAtSign('user@@example.com'));  // false
console.log(hasValidAtSign('userexample.com'));   // false

実行結果

true
false
false

パターン2: パスワード強度チェック

JavaScript
function checkPasswordStrength(password) {
  const upper   = (password.match(/[A-Z]/g) ?? []).length;
  const lower   = (password.match(/[a-z]/g) ?? []).length;
  const digits  = (password.match(/[0-9]/g) ?? []).length;
  const special = (password.match(/[^A-Za-z0-9]/g) ?? []).length;

  return {
    length: password.length,
    uppercase: upper,
    lowercase: lower,
    digits: digits,
    special: special,
    isStrong: upper > 0 && lower > 0 && digits > 0 && special > 0
  };
}

console.log(checkPasswordStrength('MyP@ss1'));

実行結果

{
  length: 7,
  uppercase: 2,
  lowercase: 2,
  digits: 1,
  special: 1,
  isStrong: true
}

パターン3: テキスト統計分析

JavaScript
function analyzeText(text) {
  return {
    totalChars:  text.length,
    letters:     (text.match(/[a-zA-Z]/g) ?? []).length,
    digits:      (text.match(/[0-9]/g) ?? []).length,
    spaces:      text.split(' ').length - 1,
    words:       text.trim().split(/s+/).length,
    sentences:   (text.match(/[.!?]+/g) ?? []).length,
    newlines:    text.split('
').length - 1
  };
}

const sample = 'Hello World! This is a test.
Line 2 here.';
console.log(analyzeText(sample));

実行結果

{
  totalChars: 42,
  letters: 30,
  digits: 1,
  spaces: 8,
  words: 9,
  sentences: 2,
  newlines: 1
}

パターン4: CSV 列数バリデーション

JavaScript
function validateCSVColumns(csvLine, expectedColumns) {
  const commaCount = csvLine.split(',').length - 1;
  const actualColumns = commaCount + 1;

  return {
    isValid: actualColumns === expectedColumns,
    actual: actualColumns,
    expected: expectedColumns
  };
}

console.log(validateCSVColumns('name,age,email', 3));
console.log(validateCSVColumns('name,age', 3));

実行結果

{ isValid: true, actual: 3, expected: 3 }
{ isValid: false, actual: 2, expected: 3 }

パターン5: HTMLタグのカウント

JavaScript
function countHTMLTags(html, tagName) {
  const regex = new RegExp(`<${tagName}[\s>]`, 'gi');
  return (html.match(regex) ?? []).length;
}

const html = '<div><p>Hello</p><p>World</p><div><p>!</p></div></div>';

console.log('p タグ:', countHTMLTags(html, 'p'));     // 3
console.log('div タグ:', countHTMLTags(html, 'div')); // 2

実行結果

p タグ: 3
div タグ: 2

ブラウザ互換性

この記事で使用したメソッドのブラウザ互換性をまとめます。

メソッド Chrome Firefox Safari Edge IE
split() 1+ 1+ 1+ 12+ 5.5+
match() 1+ 1+ 1+ 12+ 5.5+
for…of 38+ 13+ 7+ 12+ 非対応
Array.from() 45+ 32+ 9+ 12+ 非対応
スプレッド演算子 … 46+ 16+ 8+ 12+ 非対応
?? (Null合体) 80+ 72+ 13.1+ 80+ 非対応
at() メソッド 92+ 90+ 15.4+ 92+ 非対応

注意:IE対応が必要な場合は split() + match() + 基本 for ループに限定してください。for...of・スプレッド演算子・?? はIEではサポートされていません。

まとめ

JavaScriptで文字列内の特定文字の出現回数をカウントする方法を4つのパターンで解説しました。最後に、状況別のおすすめをまとめます。

やりたいこと おすすめの方法
シンプルに1文字をカウント split(ch).length - 1
大文字小文字を無視してカウント match(/ch/gi)
パターンマッチでカウント match(regex)
大量テキストの高速処理 for ループ
関数型スタイルで記述 reduce() / filter()
部分文字列(複数文字)のカウント split(sub).length - 1
重複ありの部分文字列カウント indexOf() ループ
全文字の出現頻度を取得 頻度マップ(for...of + オブジェクト)

普段使いでは split().length - 1 が最もシンプルで覚えやすい方法です。正規表現のパターンマッチが必要な場面では match() を、パフォーマンスが重要な場面では for ループを選びましょう。

関連記事