入力欄に「20文字まで」と表示し、value.length > 20 で弾く。よくある実装です。そして利用者から報告が来ます。「絵文字を1つ入れただけで、2文字分として数えられている」。
これは不具合ではありません。.length が数えているものが、最初から「文字数」ではないからです。仕様どおりに動いた結果として、画面の表示と数が合っていません。
厄介なのは、直し方が1つに決まらないところです。「正しい文字数」を出す関数へ差し替えれば済む、という形の問題ではありません。文字数には少なくとも三つの定義があり、どれを選ぶかは、その制限が何を守るためのものかによって変わります。
この記事では、MDNのString.prototype.lengthとIntl.Segmenterの記述から、三つの数え方を整理します。どちらも2026年10月30日に取得し、HTTP 200で本文を確認しました(ページ記載の最終更新は、前者が2026年9月2日、後者が2025年7月20日)。扱わないのは、言語仕様(ECMAScript)そのものの条文、見た目の1文字の境界をどう定めるかという規格の中身、Intl.Segmenter の対応ブラウザ、そして個別のデータベース製品が文字数をどう数えるかです。
30秒で要点
.lengthが返すのはUTF-16のコードユニット数である。JavaScriptの文字列はUTF-16で符号化され、1つの文字が1つまたは2つのコードユニットになる- 差を考慮する必要があるのは、絵文字・数学記号・珍しい漢字などである。ラテン文字やよく使われるCJK文字では問題にならないと明記されている
- 同じ文字列を、コードユニット・コードポイント・見た目の1文字(書記素クラスタ)の三つの単位で数えられる。値は一致しない
- 見た目の1文字で数えるには
Intl.Segmenterを使う。家族の絵文字が1と数えられる例が示されている - 静的プロパティの
String.lengthは文字列の長さとは無関係で、値は1である。同じ名前が別の量を指している
.lengthが数えているのはUTF-16のコードユニットである
MDNの定義文はこう書かれています。String 値の length プロパティは、「文字列の長さをUTF-16のコードユニット単位で」保持する。
コードユニットという語を先に置きます。JavaScriptの文字列はUTF-16という方式で符号化されており、その最小の構成単位がコードユニットです。同ページの説明部は、JavaScriptがUTF-16を使っており、1つのUnicode文字は1つまたは2つのコードユニットとして符号化され得るため、length が返す値が実際のUnicode文字数と一致しないことがあると述べています。
1文字が2つのコードユニットで表される仕組みは、サロゲートペアと呼ばれます。絵文字でこれが起きると、.length は2を返します。
ここで押さえておきたいのは、これが環境差でも不具合でもないという点です。同ページは、この挙動を仕様どおりのものとして説明しており、バグとも実装差とも書いていません。つまり「新しいブラウザなら直っている」という期待は成立しません。
どの文字でずれるのかは、言語ではなく文字で決まる
「日本語を扱うからずれる」という読み方をしがちですが、同ページの記述はそうなっていません。引用すると、ラテン文字・キリル文字・よく知られたCJK文字のような一般的な文字体系では問題にならないはずだとしたうえで、絵文字・数学記号・珍しい漢字のような一部の文字を扱う場合には、コードユニットと文字の違いを考慮する必要があるかもしれないと書かれています。
よく使われる漢字・ひらがな・カタカナは、問題にならない側に分類されています。 ずれるのは、絵文字、数学記号、そして日常的にはあまり使われない漢字です。
この区別は、実務の判断に直接効きます。日本語を扱っているから危ない、という理解では、対策の範囲が広すぎて止まります。実際に確認すべきは、その入力欄に絵文字や珍しい字が入り得るかです。氏名欄は入り得ます(異体字を含む姓は実在します)。自由記述欄も入り得ます。一方、半角英数字に限定した識別子の欄では、この問題は起きません。
同じ文字列が、数え方で三つの値になる
MDNは、数え方の違いが具体的な値の差として現れる例を挙げています。以下は同ページに記載されている期待値で、本記事でコードを実行して得た出力ではありません。
表が収まらない場合は、横にスクロールできます
| 文字列 | .length(コードユニット) | スプレッド展開(コードポイント) |
|---|---|---|
"😄" | 2 | 1 |
"𞤲𞥋𞤣𞤫"(アドラム文字) | 8 | 4 |
"∀𝑥∈ℝ,𝑥²≥0" | 11 | 9 |
二つめの数え方の仕組みは、文字列の反復です。同ページは、length は文字ではなくコードユニットを数えるので、文字数を得たい場合はまず文字列のイテレータ(1つずつ取り出す仕組み)で分割すればよいとし、[...str].length を返す関数を示しています。スプレッド展開や for...of による反復は、コードユニット単位ではなく文字単位で進みます。
ただし、これでも足りない場合があります。同ページは続けて、見た目の1文字(書記素クラスタ)の単位で数えたい場合は Intl.Segmenter を使うとし、家族を表す絵文字 "👨👩👧👧" を数えると1になる例を示しています。コード中の注記は、1つのセグメントが複数のUnicode文字から構成され得ると説明しています。
三つ並べると、関係が見えます。
- コードユニット(
.length):符号化した結果の単位数。保存や転送のサイズに近い - コードポイント(
[...str].length):Unicodeが割り当てた文字の単位数 - 書記素クラスタ(
Intl.Segmenter):人が1文字として見る単位
この三つは入れ子になっており、上から下へ行くほど値は小さくなるか等しくなります。どれが「正しい文字数」かという問いには答えがありません。答えが決まるのは、何を守りたいかを決めたあとです。
なぜ「.lengthは文字数」と信じてしまうのか
この思い込みは、不注意から来るものではありません。三つの条件が重なっています。
ひとつは、普段扱う文字では値が一致することです。前節のとおり、ラテン文字やよく使われるCJK文字では差が出ません。開発中に試す文字列も、たいていその範囲に収まります。一致する観測が何十回も積み上がったあとで、絵文字が1つ混ざった入力が来ます。反証に出会う確率が低いほど、仮説は強く信じられます。
ふたつめは、名前です。length は「長さ」としか読めません。何の長さかは名前に書かれておらず、文字列に対する長さと言われれば文字数だと読むのが自然です。同じ名前が別の量を指す例が、同じページに書かれています。静的プロパティの String.length は文字列の長さとは無関係で、String 関数の引数の個数(おおまかには仮引数の数)、すなわち1である、と明記されています。同じ length という名前が、同じ組み込みオブジェクトの中で二つの別の量を指しています。
三つめは、失敗が静かなことです。絵文字を含む入力が2文字分として数えられても、例外は出ません。入力欄が1文字早く埋まるだけです。利用者が報告してくれなければ、気づく契機がありません。
Intl.Segmenterは「分割の単位」を選ぶための仕組みである
Intl.Segmenter のページは、このオブジェクトを、ロケール(言語や地域の設定)に応じたテキスト分割を可能にし、文字列から意味のある単位(書記素・単語・文)を取り出せるようにするもの、と説明しています。segment() メソッドは、そのインスタンスのロケールと粒度に従って文字列のセグメントを表す反復可能なオブジェクトを返します。
ここで、書けることと書けないことを分けておきます。同ページにはgranularity(粒度)に指定できる値の一覧も、各値の説明もありません。 したがって「grapheme・word・sentence の3つを指定できる」と仕様として断定するには、別の文書を当てる必要があります。このページから言えるのは、三種の単位を取り出せるというところまでです。また同ページは、書記素の分割がロケールに依存すると明記しているわけではなく、この仕組み全体をロケールに応じたものと呼ぶに留まります。
単語の単位については、同ページが分かりやすい例を置いています。String.prototype.split(" ") で単語に分割しようとすると、単語の間に空白を使わないロケールのテキストでは正しい結果が得られない、として日本語・中国語・タイ語などを挙げています。例では "吾輩は猫である。名前はたぬき。" を空白で分割しても1要素のままで、"ja-JP" と単語の粒度を指定した場合には 吾輩 のように分割されることが示されています。
これは単語の粒度の例であり、文字数の数え方の話ではありません。 引き取れるのは、分割の単位そのものが言語に依存するという論点までです。
実際に何が使われたかを確かめる手立てもあります。同ページは、resolvedOptions() が初期化時に計算されたロケールと粒度を反映した新しいオブジェクトを返すと説明しています。指定したロケールがそのまま採用されたかを確認できます。ただし、どの実行環境がどのロケールに対応しているかは、このページには書かれていません。
文字列として読み込んだ時点で、エンコーディングはUTF-16になる
もう1つ、サーバー側との食い違いの原因になる注記があります。String.prototype.length のページは、他のエンコーディング(UTF-8のファイルやBlobなど)で大きな文字列を扱う場合、そのデータをJavaScriptの文字列として読み込むとエンコーディングは常にUTF-16になり、文字列のサイズは元ファイルのサイズと異なり得ると注記しています。
ここから先は、本記事側の適用です。 参照した文書はデータベースの話をしていません。そのうえで言えるのは、入力の長さを二か所で検査している構成では、両者が同じ単位で数えているとは限らないということです。画面側はUTF-16のコードユニットで数え、格納側はバイト数で上限を持っている、という組み合わせは起こり得ます。どちらが正しいかではなく、どの単位で数えているかを両側で確認するのが先です。製品ごとの具体的な挙動は、その製品の文書で確かめる対象です。
上限そのものも実装で違います。同ページは、言語仕様が文字列の最大長を2の53乗から1を引いた要素数と定める一方で、実装はそれより低い閾値を設けており、その値が実行環境ごとに異なると記載しています。入力欄の文字数制限とは桁が違う話なので混ぜる必要はありませんが、単位も上限も一つに決まっていないという点では同じ性質です。
判断軸:その「文字数」は、何を守るための数か
数え方を選ぶ順序を、次のように置くと決まります。
- その制限は何を守っているのかを言葉にする。 表示の崩れを防ぐためか、格納できる容量を守るためか、読む人の負担を抑えるためか。ここが決まらないうちは、どの単位が正しいかも決まりません
- 表示の崩れを防ぐなら、人が1文字と見る単位で数える。 画面に並ぶ数を抑えたいのであれば、書記素クラスタの単位が意図に近くなります
- 格納できる容量を守るなら、格納側の単位に合わせる。 画面側で数えた値が、格納側の上限と同じ単位であるかを確認します。違う単位なら、画面側の制限は上限の保証になりません
- 利用者に見せる数と、内部で検査する数を分けてよい。 「残り5文字」と表示する数は人が数える単位で、拒否の判定は格納側の単位で、という構成は矛盾しません。矛盾するのは、両方を同じ数だと説明してしまう場合です
- 絵文字や珍しい字が入り得る欄かどうかを見る。 入り得ない欄では、この区別に手間をかける必要はありません
この5つのうち最初の1つを飛ばすと、残りは決められません。 「正しい文字数を数える関数」を探し始めたときは、1番目に戻る合図です。
手元で確かめられる最小の手順
いま動いている入力欄について、1つだけ確かめられます。
- 対象の入力欄に、絵文字を1つだけ入力して送信する
- 画面側が表示した残り文字数(または文字数カウンタの値)を記録する
- 格納された値の長さを、格納側が数える単位で記録する
- 二つの数が一致するかを見る
一致しなければ、画面側の制限は格納側の上限の保証になっていません。これは不具合の発見ではなく、二か所が別の単位で数えていることの確認です。
数えるなら、定義を揃えます。
- 分子:絵文字を1つ含む入力について、画面側の文字数と格納側の長さが一致した欄の数
- 分母:文字数制限を持つ入力欄の総数
- 単位:件/件(割合で見る場合は%)
この比率を上げること自体が目的ではありません。一致していない欄があると分かれば十分です。 そのうえで、前節の1番目に戻り、その欄の制限が何を守るためのものかを決めます。守るものが表示の崩れだけなら、一致していなくても構わないという判断もあり得ます。
確かなことと、確かでないこと
確かなこと(2026年10月30日に2ページを取得して本文で確認した範囲):.length がUTF-16のコードユニット数を返すこと。1つのUnicode文字が1つまたは2つのコードユニットになり得るため値が一致しないことがあること。差を考慮すべき文字として絵文字・数学記号・珍しい漢字が挙げられ、ラテン文字・キリル文字・よく知られたCJK文字は問題にならない側に分類されていること。文字列のイテレータで分割すると文字単位で数えられること。書記素クラスタで数えるには Intl.Segmenter を使い、家族の絵文字が1になる例が示されていること。Intl.Segmenter がロケールに応じた分割を行い、resolvedOptions() で計算されたロケールと粒度を確認できること。静的プロパティの String.length が引数の個数(1)であること。他のエンコーディングのデータを文字列として読み込むとUTF-16になり、サイズが元ファイルと異なり得ること。.length 側のページに「Baseline Widely available」(2015年7月以降、各ブラウザで利用可能)の表示があること。
確かでないこと:表に挙げた値は参照した文書に記載された期待値であり、本記事でコードを実行して得たものではありません。granularity に指定できる値の一覧(参照したページに記載がありません)。Intl.Segmenter の対応状況(対応表を今回読めていません)。見た目の1文字の境界をどう定めるかという規格の中身(未参照)。個別のデータベース製品が文字数とバイト数のどちらで上限を持つか、照合順序がそれに影響するか。
この記事が踏み込んでいないこと:単位を決めずに数えると値が揃わないという構図は、文字数に限りません。識別子の長さを「1〜40 Unicodeコードポイント」と定める例はサーバーレスアーキテクチャ入門で触れており、本記事はその「なぜ見える文字数と一致しないのか」を引き取った形です。時刻で同じことが起きる例はUTCだけでは足りない、符号化と保護の取り違えはJWTは暗号化ではない、集計の単位がそろわない例はパーセンタイルは平均できないで扱っています。
入力制限を決める前に立ち返る問い
- この数は、誰に見せる数か。 利用者に見せる数と、拒否の判定に使う数は、同じでなくて構いません
- この制限が守っているものは何か。 表示か、容量か、読む負担か。言葉にできないなら、まだ単位を選べません
- 数えている場所は何か所あるか。 二か所以上あるなら、単位が同じかを確かめる対象です
三つめは、文字数に限った話ではありません。同じ量を二か所で数えている構成では、単位の一致を誰も検査していないことがあります。合わないと分かった時点で選べる選択肢は、揃えることだけではありません。 揃えないと決めたうえで、どちらの数を利用者に見せるかを決める、という判断もあります。
参考資料・引用元
- String.prototype.length — MDN(2026年10月30日に取得・HTTP 200。ページ記載の最終更新は2026年9月2日)
- Intl.Segmenter — MDN(2026年10月30日に取得・HTTP 200。ページ記載の最終更新は2025年7月20日)