Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】JavaScript で人と同じように文字数を数える

2025年09月22日に「Zenn」が公開したITニュース「JavaScript で人と同じように文字数を数える」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

JavaScriptでは、絵文字などの特殊な文字を数える際、人間とは異なる文字数になる場合がある。これは文字の内部的な表現(エンコーディング)の違いが原因で、人間と同じように正確に数えるための方法がある。

ITニュース解説

JavaScriptで文字数を数える際に、人間が直感的に数えるのと異なる結果になることがある。例えば、「A」は1文字とJavaScriptも認識するが、「😆」(顔文字)は2文字、「👨‍👩‍👧‍👦」(家族の絵文字)に至っては11文字と数える場合がある。なぜこのような違いが生まれるのか、そしてJavaScriptに人間と同じように文字を数えさせるにはどうすれば良いのかを解説する。

この違いの根本的な理由は、コンピュータが文字をどのように扱っているかにある。コンピュータは文字を直接理解できず、すべてを数字(ビット)で処理する。そのため、各文字には固有の数字が割り当てられている。この対応表が「文字集合」であり、数字をコンピュータが扱える形に変換するルールが「文字エンコーディング」だ。 初期のコンピュータでは、英数字と記号を扱うためのASCII(アスキー)という文字集合が広く使われた。しかし、これは日本語や他の国の言語を表現できないため、より多くの文字を扱える文字集合が必要になった。そこで登場したのがUnicode(ユニコード)である。Unicodeは世界中のほとんどの文字に一意の番号(コードポイント)を割り当てることを目指している。 Unicodeのコードポイントをコンピュータに保存・伝送するために使われるエンコーディング方式がいくつかある。よく知られているのはUTF-8(ユーティーエフエイト)やUTF-16(ユーティーエフシックスティーン)だ。 UTF-8は、1文字を表すのに1バイトから4バイトまで可変長のバイト数を使う。英語の文字は1バイト、日本語や多くの絵文字は3バイト、ごく一部の絵文字は4バイトで表現されることが多い。Webサイトや多くのシステムで標準的に利用されている。 一方、JavaScriptの内部では文字列をUTF-16で扱っていることが多い。UTF-16は、基本的に1文字を2バイト(16ビット)で表現する。しかし、Unicodeには2バイトでは表現しきれない文字、特に多くの絵文字や一部の珍しい漢字が存在する。

UTF-16の2バイトでは表現できない文字を扱うため、「サロゲートペア」という仕組みが導入された。これは、2つの2バイト(合計4バイト)を使って1つの文字を表現する方法である。例えば、前述の「😆」という絵文字は、UTF-16ではサロゲートペアによって2つの2バイトの単位、つまり4バイトで表現される。JavaScriptのlengthプロパティは、この2バイト単位の数を数えるため、「😆」を2文字とカウントしてしまうのだ。人間から見れば「😆」は1つのまとまった文字だが、JavaScriptのlengthプロパティは内部的なUTF-16のコード単位を数えているに過ぎない。

さらに複雑なのが「👨‍👩‍👧‍👦」のような家族の絵文字や、肌の色のバリエーションを持つ絵文字だ。これらは単一のコードポイントで表されるのではなく、複数のコードポイントが組み合わさって1つの意味を持つ文字として表示される「結合文字シーケンス」(または絵文字シーケンス)である。 例えば、「👨‍👩‍👧‍👦」は、「男性」「女性」「女の子」「男の子」の絵文字コードポイントと、それらを結合するための「ゼロ幅結合子」(Zero Width Joiner, ZWJ)という特殊な制御文字が複数組み合わさってできている。このため、JavaScriptのlengthプロパティが各構成要素のUTF-16コード単位を数えると、合計で11という大きな数字になることがある。これも人間が直感的に数える1文字とは大きく異なる結果だ。

JavaScriptで人間と同じように「見かけ上の1文字」として文字数を数えるには、これらの内部的なコード単位や結合文字の仕組みを考慮する必要がある。 一つの方法は、文字列をUnicodeの「書記素クラスタ」(人間が認識する文字単位)に分割し、その数を数えることだ。これはArray.from()メソッドやスプレッド構文([...str])を使って実現できる。これらの方法は、内部的にJavaScriptエンジンが文字列をUnicodeの書記素クラスタに分解してくれるため、「😆」や「👨‍👩‍👧‍👦」のような文字も正しく1文字と数えることができる。 具体的な例を挙げると、Array.from("😆").length[..."😆"].lengthを実行すると、結果は1となる。同様に、Array.from("👨‍👩‍👧‍👦").length[..."👨‍👩‍👧‍👦"].lengthを実行すると、結果は1となる。これは、これらのメソッドがサロゲートペアや結合文字シーケンスを1つの書記素クラスタとして認識するためだ。

より高度な解決策として、Intl.Segmenter(インターナショナル・セグメンター)APIを利用する方法もある。このAPIは、国際化(i18n)の文脈でテキストを正確に区切るために設計されており、書記素クラスタだけでなく、単語や文といったより大きな単位でテキストを分割することも可能だ。 Intl.Segmenterを使うと、指定した言語のルールに基づいてテキストを正確にセグメント化(分割)できる。例えば、new Intl.Segmenter().segment("👨‍👩‍👧‍👦")のようにしてセグメンターオブジェクトを生成し、その結果のイテレータから要素を数えることで、より堅牢に書記素クラスタ単位の数を取得できる。このAPIは、単に文字数を数えるだけでなく、特定の言語における改行ルールや単語の区切りなど、より複雑なテキスト処理が必要な場合に特に有効だ。

JavaScriptのlengthプロパティが期待通りに動かないのは、コンピュータが文字を内部的にどのように扱っているかという技術的な背景があるためだ。UTF-16のコード単位、サロゲートペア、そして結合文字シーケンスといった概念が、人間とJavaScriptの文字数カウントの違いを生み出している。 しかし、Array.from()やスプレッド構文、あるいはIntl.Segmenter APIといったツールを使うことで、これらの内部的な複雑さを気にすることなく、人間が直感的に認識する「見かけ上の文字数」を正確に数えることが可能になる。Webアプリケーションやサービスの開発において、ユーザーに表示する文字数制限のチェックや、テキスト処理の際にこの知識は非常に重要となるだろう。正しい文字数カウントは、アプリケーションの国際化対応や、ユーザー体験の向上に直結する技術的な課題の一つと言える。例えば、SNSの投稿文字数制限を正確にユーザーに提示する場合や、テキストエディタで入力中の文字数をリアルタイムに表示する場合など、ユーザーが違和感なくサービスを利用できるようにするためには、この正確な文字数カウントの技術が不可欠である。

関連コンテンツ