もくじ
JavaScriptで文字数をカウントしたい場面はよくあります。
特にフォーム入力で「○○文字まで」と表示したり、バリデーションを行ったりする際に使います。
しかし一口に「文字数」といっても、状況によってカウントの仕方が異なります。
この記事では、基本的なカウント方法から、絵文字・合成文字(複数の絵文字を組み合わせたもの)に対応した、高精度なカウント方法まで丁寧に解説します。
基本的な文字数のカウント方法
JavaScriptで文字数を数えるには、文字列の.lengthプロパティを使います。
Copyをクリックするとコピーできます。
const text = 'こんにちは、世界!';const count = text.length;console.log(count); // 出力: 9一見、これで問題なさそうですが、絵文字や特殊文字に対応できていないことがあります。
- 半角・全角問わず、1文字としてカウントされます。
- サロゲートペア(絵文字や一部の漢字など)で見た目は1文字なのに、2文字とカウントされることがあります。
.lengthでは正確にカウントできない文字が存在する
JavaScriptの.lengthは、UTF-16コードユニットの数を返します。
そのため、サロゲートペア(絵文字や一部の漢字など)を含むと「1文字に見えるのに2文字と数える」ことがあります。
Copyをクリックするとコピーできます。
const emoji = '😊';const count = emoji.length;console.log(count); // 2 ← 1文字に見えるのに2文字とカウント[...text].lengthでサロゲートペア(絵文字や一部の漢字など)のカウントに対応する
スプレッド構文を使うと、文字列をコードポイント単位で分割できます。
Copyをクリックするとコピーできます。
const emoji = '😊';const count = [...emoji].length;console.log(count); // 1 ← 正しく1文字とカウント[...text].lengthでは正確にカウントできない文字が存在する
[...text].lengthで多くの絵文字のカウントに対応できますが、次のような合成文字(複数の絵文字を組み合わせたもの)はカウントできません。
Copyをクリックするとコピーできます。
const complexEmoji = '👨👩👧👦';const count = [...complexEmoji].length;console.log(count); // 7 ← 1文字に見えるのに7文字とカウント絵文字や合成文字などを人間が見た目で認識する1文字として正確にカウントする方法
「人間が見た目で認識する1文字(grapheme cluster)」をカウントしたい場合は、次の2つの方法があります。
Intl.Segmenterで正確にカウントする
Copyをクリックするとコピーできます。
const segmenter = new Intl.Segmenter('ja', { granularity: 'grapheme' });const count = [...segmenter.segment('👨👩👧👦')].length;console.log(count); // 1Intl.Segmenterのブラウザ別対応状況
Intl.SegmenterはAPIの一部でモダンブラウザに対応してますが、古いブラウザで使用できない場合があります。
Intl.Segmenterのブラウザ別対応状況は、こちらから確認できます。
ライブラリ(grapheme-splitter)を使用して正確にカウントする
Copyをクリックするとコピーできます。
const splitter = new GraphemeSplitter();const complexEmoji = '👨👩👧👦';const count = splitter.countGraphemes(complexEmoji);console.log(count); // 1grapheme-splittの使用方法
npmインストールやCDN読み込み、githubからファイルをダウンロードするなどでライブラリの読み込みが必要です。
以下はCDN読み込みを使用した実装例です。
Copyをクリックするとコピーできます。
<script src="https://cdn.jsdelivr.net/npm/grapheme-splitter@1.0.4/index.min.js"></script><script> const splitter = new GraphemeSplitter(); const complexEmoji = '👨👩👧👦'; const count = splitter.countGraphemes(complexEmoji); console.log(count); // 1</script>各カウント方法の改行文字の扱いとmaxlengthとの一致比較
どの方法でも改行\nは「1文字」とカウントされますが、maxlengthの基準は.lengthなので、他の方法とはズレることがあります。
| カウント方法 | 改行(\n)の扱い | maxlengthと一致するか | 特徴 |
|---|---|---|---|
| .length | 1文字と数える | 一致する | 最も基本 UTF-16コードユニット数 |
| [...text].length | 1文字と数える | 一致しない | 絵文字(サロゲートペア)に対応 合成文字(複数の絵文字を組み合わせたもの)には対応していない |
| Intl.Segmenter | 1文字と数える | 一致しない | 高精度 合成文字(複数の絵文字を組み合わせたもの)に対応 古いブラウザで使用できないことがある |
| grapheme-splitter | 1文字と数える | 一致しない | 高精度 合成文字(複数の絵文字を組み合わせたもの)に対応 古いブラウザでも使用できる |
実際に入力して文字数の違いを比較してみよう
textareaのmaxlengthは、100に設定しています。
コピー用テキスト:😊 👨👩👧👦 𩸽 𝄞
.length 0
[...text].length 0
Intl.Segmenter 0
GraphemeSplitter 0
【実装コード】実際に入力して文字数の違いを比較してみよう
Copyをクリックするとコピーできます。
<textarea id='text-input' class="text-input" rows="5" cols="50" placeholder="ここに入力してみてください(例:😊👨👩👧👦など)" maxlength="100"></textarea><div class="counter-box"> <p class="counter-txt"> <span>.length</span> <span id='length-count'>0</span> </p> <p class="counter-txt"> <span>[...text].length</span> <span id='spread-count'>0</span> </p> <p class="counter-txt"> <span>Intl.Segmenter</span> <span id='segmenter-count'>0</span> </p> <p class="counter-txt"> <span>GraphemeSplitter</span> <span id='grapheme-count'>0</span> </p></div><!-- GraphemeSplitter CDN --><script src="https://cdn.jsdelivr.net/npm/grapheme-splitter@1.0.4/index.min.js"></script><script> const input = document.getElementById('text-input'); const lengthCount = document.getElementById('length-count'); const spreadCount = document.getElementById('spread-count'); const segmenterCount = document.getElementById('segmenter-count'); const graphemeCount = document.getElementById('grapheme-count'); const splitter = new GraphemeSplitter(); function updateCounts() { const text = input.value; // .length lengthCount.textContent = text.length; // [...text].length spreadCount.textContent = [...text].length; // Intl.Segmenter(対応ブラウザのみ) if (typeof Intl.Segmenter !== 'undefined') { const segmenter = new Intl.Segmenter('ja', { granularity: 'grapheme' }); const segments = [...segmenter.segment(text)]; segmenterCount.textContent = segments.length; } else { segmenterCount.textContent = '未対応'; } // GraphemeSplitter graphemeCount.textContent = splitter.countGraphemes(text); } input.addEventListener('input', updateCounts);</script>.text-input { width: 100%; margin-bottom: 1em; padding: 0.5em; resize: vertical; font-family: inherit; font-size: 1rem;}.counter-txt { display: flex; flex-wrap: wrap;}.counter-txt span:first-child { width: 10em; position: relative; white-space: nowrap;}.counter-txt span:first-child::after { position: absolute; top: 0; right: 1.5em; content: ":";}まとめ
今回はフロントエンドの文字数カウントについて解説しました。
バックエンド(PHP, Python, Java, Rubyなど)やデータベースと連携が必要な場合、「何を1文字とするか」は言語や設定によって異なります。
そのため、文字数制限をフロント・バック両方で共通にしたい場合は、同じアルゴリズム(grapheme単位)を使うのが理想です。
改行もすべての方法で「1文字」扱いされる
maxlengthは.lengthを基準にしている見た目通りの文字数を数えたいなら
Intl.Segmenterorgrapheme-splitterバックエンドやDBと連携する場合、文字数制限に注意が必要
文字数のカウントは一見シンプルですが、絵文字や特殊文字、漢字を扱う際は注意が必要です。
今回紹介した方法を使えば、実用的なカウント機能を実装できるはずです。
ぜひ、あなたのWebアプリにも活用してみてください!
