もくじ
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); // 1
Intl.Segmenterのブラウザ別対応状況
Intl.Segmenter
はAPIの一部でモダンブラウザに対応してますが、古いブラウザで使用できない場合があります。
Intl.Segmenter
のブラウザ別対応状況は、こちらから確認できます。
ライブラリ(grapheme-splitter)を使用して正確にカウントする
Copyをクリックするとコピーできます。
const splitter = new GraphemeSplitter();
const complexEmoji = '👨👩👧👦';
const count = splitter.countGraphemes(complexEmoji);
console.log(count); // 1
grapheme-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.Segmenter
orgrapheme-splitter
バックエンドやDBと連携する場合、文字数制限に注意が必要
文字数のカウントは一見シンプルですが、絵文字や特殊文字、漢字を扱う際は注意が必要です。
今回紹介した方法を使えば、実用的なカウント機能を実装できるはずです。
ぜひ、あなたのWebアプリにも活用してみてください!