JavaScriptで文字列内の特定文字の出現回数をカウントするには、split().length - 1・match()・forループ・reduce()の4つのパターンがあります。
この記事では、それぞれの方法の使い方と違いを比較し、大文字小文字の無視・部分文字列のカウント・出現頻度マップの作成・パフォーマンス比較・実務パターンまで体系的に解説します。
この記事で学べること
split()・match()・for・reduce() による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
"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
注意: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
マッチなしの場合の安全な書き方
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
ポイント: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
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
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
ポイント: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
filter() を使ったワンライナー
reduce() の代わりに filter() を使う方法もあります。マッチする文字だけを抽出してその長さを取ります。
JavaScript
const countChar = (str, char) =>
[...str].filter(c => c === char).length;
console.log(countChar('abracadabra', 'a')); // 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回)
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
部分文字列のカウント(複数文字の出現回数)
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
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
注意: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
最頻出文字・最少文字の取得
頻度マップを基に、最も多く出現する文字と最も少ない文字を取得する方法です。
最頻出文字を取得
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
パターン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
ブラウザ互換性
この記事で使用したメソッドのブラウザ互換性をまとめます。
| メソッド |
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 ループを選びましょう。
関連記事