【PR】を含みます。

フロントエンド

【JavaScript】文字数をカウントする方法

JavaScript 文字数をカウントする方法

JavaScriptで文字数をカウントしたい場面はよくあります。

特にフォーム入力で「○○文字まで」と表示したり、バリデーションを行ったりする際に使います。

しかし一口に「文字数」といっても、状況によってカウントの仕方が異なります。

この記事では、基本的なカウント方法から、絵文字・合成文字(複数の絵文字を組み合わせたもの)に対応した、高精度なカウント方法まで丁寧に解説します。

基本的な文字数のカウント方法

JavaScriptで文字数を数えるには、文字列の.lengthプロパティを使います。

Copyをクリックするとコピーできます。

JavaScript
Copy
const text = 'こんにちは、世界!';
const count = text.length;
console.log(count); // 出力: 9

一見、これで問題なさそうですが、絵文字や特殊文字に対応できていないことがあります。

POINT
  • 半角・全角問わず、1文字としてカウントされます。
  • サロゲートペア(絵文字や一部の漢字など)で見た目は1文字なのに、2文字とカウントされることがあります。

.lengthでは正確にカウントできない文字が存在する

JavaScriptの.lengthは、UTF-16コードユニットの数を返します。

そのため、サロゲートペア(絵文字や一部の漢字など)を含むと「1文字に見えるのに2文字と数える」ことがあります。

Copyをクリックするとコピーできます。

JavaScript
Copy
const emoji = '😊';
const count = emoji.length;
console.log(count); // 2 ← 1文字に見えるのに2文字とカウント

[...text].lengthでサロゲートペア(絵文字や一部の漢字など)のカウントに対応する

スプレッド構文を使うと、文字列をコードポイント単位で分割できます。

Copyをクリックするとコピーできます。

JavaScript
Copy
const emoji = '😊';
const count = [...emoji].length;
console.log(count); // 1 ← 正しく1文字とカウント

[...text].lengthでは正確にカウントできない文字が存在する

[...text].lengthで多くの絵文字のカウントに対応できますが、次のような合成文字(複数の絵文字を組み合わせたもの)はカウントできません。

Copyをクリックするとコピーできます。

JavaScript
Copy
const complexEmoji = '👨‍👩‍👧‍👦';
const count = [...complexEmoji].length;
console.log(count); // 7 ← 1文字に見えるのに7文字とカウント

絵文字や合成文字などを人間が見た目で認識する1文字として正確にカウントする方法

「人間が見た目で認識する1文字(grapheme cluster)」をカウントしたい場合は、次の2つの方法があります。

Intl.Segmenterで正確にカウントする

Copyをクリックするとコピーできます。

JavaScript
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をクリックするとコピーできます。

JavaScript
Copy
const splitter = new GraphemeSplitter();
const complexEmoji = '👨‍👩‍👧‍👦';
const count = splitter.countGraphemes(complexEmoji);
console.log(count); // 1

grapheme-splittの使用方法

npmインストールやCDN読み込み、githubからファイルをダウンロードするなどでライブラリの読み込みが必要です。

以下はCDN読み込みを使用した実装例です。

Copyをクリックするとコピーできます。

HTML
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と一致するか特徴
.length1文字と数える一致する最も基本
UTF-16コードユニット数
[...text].length1文字と数える一致しない絵文字(サロゲートペア)に対応
合成文字(複数の絵文字を組み合わせたもの)には対応していない
Intl.Segmenter1文字と数える一致しない高精度
合成文字(複数の絵文字を組み合わせたもの)に対応
古いブラウザで使用できないことがある
grapheme-splitter1文字と数える一致しない高精度
合成文字(複数の絵文字を組み合わせたもの)に対応
古いブラウザでも使用できる

実際に入力して文字数の違いを比較してみよう

textareaのmaxlengthは、100に設定しています。

コピー用テキスト:😊 👨‍👩‍👧‍👦 𩸽 𝄞

.length 0

[...text].length 0

Intl.Segmenter 0

GraphemeSplitter 0

【実装コード】実際に入力して文字数の違いを比較してみよう

Copyをクリックするとコピーできます。

HTML
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>
CSS
Copy
.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アプリにも活用してみてください!

-フロントエンド
-