
1. イントロダクション
「文字化け」――それはWeb開発における、かつての“風物詩”のようなものです。
メールの件名が謎の記号列になる、外部サービスと通信すると文字がぐちゃぐちゃになる……
こうした現象の多くは、「文字コード」の理解不足から生じています。
前回の記事で触れたこと
前回の記事「[Unicodeの歴史]」では、文字コードの発展とUnicodeの登場までを振り返りました。
![[Unicode] Unicodeとは何か ― 歴史とWindows/Python実例](https://humanxai.info/images/uploads/unicode-history.webp)
[Unicode] Unicodeとは何か ― 歴史とWindows/Python実例
文字化けの原因を歴史的背景から整理し、WindowsとPythonにおけるUnicodeの落とし穴と実務的な回避策を具体例とともに解説する記事。Real-ESRGANを日本語ファイル名で動かした際のトラブルをケーススタディに、ASCII一時退避やUTF-8統一 …
https://humanxai.info/posts/unicode-history/- ASCII や Shift_JIS のようなローカル文字コードの限界
- 多言語化(国際化)の流れの中で Unicode が生まれた背景
- 「1文字=1バイト」では成り立たない世界へ突入したこと
この知識は、現在のJavaScript開発においても不可欠な土台となります。
なぜUnicodeは避けて通れないのか?
JavaScriptでは、すべての文字列が内部的にUnicode(UTF-16)で扱われています。
一見すると「文字列はただの文字列」と思いがちですが、実際には1文字が2つの単位(コードユニット)に分かれていたり、見た目は同じなのに内部表現が異なることもあります。
その結果:
.length
が期待とズレる- 絵文字や特殊文字が正しく切り出せない
- 正規表現で意図したマッチができない
- 通信やデータ保存で文字化けが起きる
……といったトラブルが起きます。
本記事の目的
この講座では、「JavaScript × Unicode」の基本を丁寧に押さえながら、実践で避けたい罠や、便利なテクニックを紹介します。
難解に見えるUnicodeの世界を、JavaScriptの文法やAPIを通じて、楽しく・安全に扱えるようになることが目標です。
👉 次章では、まずJavaScriptがどのようにUnicodeを扱っているかの基本構造から見ていきましょう。
2. JavaScriptの文字列はUnicodeが基本
JavaScriptでは、すべての文字列(String
型)は内部的にUnicode(UTF‑16)で表現されています。
これはつまり、「JavaScriptの文字列操作=Unicodeの扱い」と言っても過言ではありません。
文字列の正体:UTF‑16コードユニットの配列
JavaScriptの文字列は、厳密には「UTF‑16でエンコードされたコードユニットの配列」としてメモリに格納されています。
const str = "A";
console.log(str.charCodeAt(0)); // 65(=U+0041)
この例の "A"
は1つのコードユニット(16bit)で表現できる、いわゆる BMP(基本多言語面) に含まれる文字です。
const emoji = "😄"; // U+1F604
console.log(emoji.length); // 2
一方、"😄"
のような絵文字は サロゲートペア(後述) で表現され、2つの16bitユニット(計32bit) で1文字が構成されます。
.length に潜む罠
console.log("A".length); // 1
console.log("😄".length); // 2 ← 見た目は1文字なのに!
console.log("👨👩👧👦".length); // 11 ← ファミリー絵文字(ZWJシーケンス)
String.prototype.length
はコードユニット数であり、「見た目の文字数」ではありません。- つまり、「1文字」=「1 length」ではない ことに注意が必要です。
なぜUTF‑16なのか?
UTF‑16は、Unicodeを効率的に扱うために開発された可変長エンコーディング方式です。
- U+0000〜U+FFFF(BMP) の範囲は1ユニット(16bit)で表現
- U+10000以上(補助面) の文字はサロゲートペア(2ユニット=32bit)で表現
JavaScriptではECMAScript仕様により、文字列はこのUTF‑16方式で扱われることが決まっており、エンジン(V8、SpiderMonkeyなど)に関係なく共通の挙動になります。
実際の影響は?
この仕組みを知らずに文字列を扱うと、以下のようなバグが起きやすくなります:
- 文字を1つずつ処理しているつもりが、絵文字や一部の漢字で文字化けや分割エラーが発生
- 正規表現で意図したマッチができない(例:
.
が絵文字を分割する) - スプレッド演算子や
Array.from()
を使って正しく処理しないと、ユーザー入力のバリデーションが壊れる
結論
JavaScriptの文字列は見た目通りではない。
「見える1文字」が、内部では複数のコードユニットにまたがっていることを常に意識することが重要です。
3. サロゲートペアと結合文字
「JavaScriptの文字列はUnicodeで表現される」と聞いても、最初はピンとこないかもしれません。
しかし実際に扱うと、「1文字扱いしたのに分裂した」「思ったより .length
が多い」といった問題に直面します。
その正体こそが――サロゲートペアと結合文字です。
🔹 サロゲートペアとは?
定義:
- UTF-16では、U+10000 以上の文字(補助面)を表すために、**2つの16bitの値(=コードユニット)**で1文字を構成します。
- この「2つで1文字」をまとめたものが、サロゲートペアです。
🧪 例:
const emoji = "😄"; // U+1F604
console.log(emoji.length); // 2
console.log(emoji.charCodeAt(0).toString(16)); // d83d
console.log(emoji.charCodeAt(1).toString(16)); // de04
"😄"
は\uD83D\uDE04
の2つで構成されており、見た目は1文字でも.length
は2になります。
落とし穴
console.log(emoji[0]); // "\uD83D" → 無意味な半分だけの文字
これを無理に .slice()
や .split("")
で処理すると、サロゲートが分離されてバグる原因になります。
🔹 結合文字とは?
定義:
- 2つ以上の文字が視覚的には1つの文字として表示される組み合わせ。
- 例:
e
+ アクセント記号◌́
→ 見た目はé
例:
const combined = "e\u0301"; // U+0065 + U+0301
console.log(combined); // 表示は é
console.log(combined.length); // 2(コードユニットでも2)
一方、"é"
のプリミティブ文字(U+00E9)は .length === 1
。
つまり:
見た目 | 内部構造 | .length |
---|---|---|
é (単体) |
U+00E9 | 1 |
e + ◌́ |
U+0065 + U+0301 | 2 |
この2つがもたらす罠
処理 | 問題の例 |
---|---|
split("") |
サロゲートペアや結合文字を分解して壊す |
.charAt(n) |
見た目と違う文字を返す("👨👩👧👦" は分割不能) |
.slice() |
Unicodeの境界を壊しやすい |
バリデーション | 絵文字入り名前などの「1文字制限」で落ちる |
🛠 正しく扱うには?
今後の章で詳しく紹介しますが、ここでは軽く対処法を紹介します:
// スプレッドで安全に分解(見た目の文字単位)
const graphemes = [..."👨👩👧👦"];
console.log(graphemes.length); // 1(見た目どおり)
または、より厳密に扱いたい場合は、
Intl.Segmenter
や grapheme-splitter
ライブラリの活用も選択肢です。
まとめ
- サロゲートペア:補助面の文字(例:絵文字)は2コードユニットで構成される
- 結合文字:複数のUnicode文字が1つの“見た目の文字”になる
- JavaScriptでは、
.length
や.slice()
が「見た目通りに動かない」ことがある - 文字列操作では、常に“Unicode境界”に注意が必要
4. エスケープシーケンスと文字リテラル
JavaScriptのコード中で直接書けない文字や特殊文字を表す方法が、**エスケープシーケンス(escape sequences)**です。 Unicode文字も、さまざまな方法でエスケープして記述できます。 この章では、3種類の表記法とその違い、使いどころを整理します。
\xHH(1バイト・2桁の16進数)
console.log("\x41"); // "A"
- 範囲:
\x00
〜\xFF
(=0〜255) - ASCII範囲のみ対応
- 日本語やEmojiなど、多バイト文字は使えない
- ほぼ過去互換・限定用途向け
\uXXXX(4桁の16進数)
console.log("\u3042"); // "あ"
console.log("\u2764"); // "❤"
- 範囲:
\u0000
〜\uFFFF
(BMP内) - 基本多言語面(BMP)までのUnicode文字を記述可能
- サロゲートペアが必要な補助面の文字(Emojiなど)には不十分
例:😄(U+1F604)は \uD83D\uDE04
のペアで表現する必要あり
console.log("\uD83D\uDE04"); // 😄
\u{XXXXX}(コードポイント形式:ES2015以降)
console.log("\u{1F604}"); // 😄
console.log("\u{2764}\u{FE0F}"); // ❤️
- Unicodeコードポイントそのものを指定できる
- 1〜6桁の16進数まで対応(補助面もOK)
- サロゲートペアを意識せず、自然な書き方ができる
use strict
モードでも利用可能(ES2015+)
よくある誤解:「文字コード=見た目」ではない
console.log("\u00e9"); // é (1文字)
console.log("e\u0301"); // é (合成:2文字)
console.log("é".length); // 1
console.log("e\u0301".length); // 2
両方とも表示は「é」ですが、内部表現も.length
も異なります。
この違いが、**文字列比較や検証での“罠”**になります。
注意点
記法 | サポート | 備考 |
---|---|---|
\xHH |
古いJSから対応 | ASCIIのみ/実用性は低い |
\uXXXX |
全環境対応 | BMPまでOK/サロゲート手動必要 |
\u{XXXXX} |
ES2015以降 | 一番推奨。コードポイントをそのまま扱える |
まとめ
- JSではエスケープシーケンスを使って文字を数値として記述できる
- 特に
\u{}
記法を使うと、補助面の文字やEmojiも簡潔に書ける - 見た目が同じでも、内部構造は異なることがある(正規化・比較に注意)
5. 安全に文字列操作する方法
前章までで、JavaScriptの文字列は単なる“1文字ずつ”の配列ではなく、
サロゲートペアや結合文字など、視覚と内部構造が一致しないケースがあると紹介しました。
この章では、それらを安全に扱うための実践的な手法をご紹介します。
危険な操作例(やりがち)
const emoji = "😄";
console.log(emoji.length); // 2
console.log(emoji[0]); // "\uD83D" ← サロゲートの“半分”
const chars = "👨👩👧👦".split("");
console.log(chars); // バラバラに壊れる
これらの操作は、「1見た目の文字=1コードユニット」 という誤解に基づくものです。 絵文字・合成文字(ZWJ)・アクセント文字などは壊れてしまいます。
安全な操作方法
[…str](スプレッド構文)
const chars = [..."😄👍🏽é"];
console.log(chars); // ["😄", "👍", "🏽", "é"]
- コードポイント単位で文字を分解
- サロゲートペアを1つの見た目文字として扱える
for...of
と同じ動き(後述)
Array.from(str)
const chars = Array.from("😄👍🏽é");
console.log(chars); // ["😄", "👍", "🏽", "é"]
- スプレッド構文と同様にコードポイントベース
.map()
や.filter()
などと組みやすい
for…of ループ
for (const ch of "👨👩👧👦") {
console.log(ch);
}
// 1回だけ実行される(文字単位で正しく処理)
- 内部的にコードポイントを認識
.length
に頼らない文字数処理が可能
正規表現 + u フラグ(Unicode aware)
const str = "😄👍🏽";
const matched = str.match(/./gu);
console.log(matched); // ["😄", "👍", "🏽"]
g
はグローバル、u
は Unicodeモード- サロゲートペアや合成文字も安全に分割
正規表現で Unicode プロパティクラスを使う(ES2018+)
const letters = "あA1".match(/\p{L}/gu);
console.log(letters); // ["あ", "A"]
\p{L}
は文字にマッチ(L
=Letter)- 他にも
\p{Emoji}
や\p{Script=Katakana}
など高度な分類が可能 u
フラグ必須
Intl.Segmenter(より正確な分割)
const segmenter = new Intl.Segmenter("ja", { granularity: "grapheme" });
const segments = [...segmenter.segment("こんにちは👋")];
console.log(segments.map(s => s.segment)); // ["こ", "ん", "に", "ち", "は", "👋"]
- グラフェム単位(見た目の1文字)で分割可能
- 多言語対応、高精度
- モダンブラウザ(Chrome/Edge/Firefox/Safari)対応済
目的別まとめ
目的 | 使うべき手法 |
---|---|
見た目の文字ごとに分割 | [...str] , Array.from() , for...of , Intl.Segmenter |
高度なフィルタ(絵文字/漢字など) | 正規表現 + \p{} |
「1文字ずつ処理」でも壊したくない | for...of , match(/./gu) , Segmenter |
注意:String.prototype.charAt() や .slice() などは非推奨
これらはサロゲートペアや結合文字を正しく扱えません:
"😄".charAt(0); // "\uD83D" ← NG
"😄".slice(0, 1); // "\uD83D" ← NG
安全に使いたい場合は、.codePointAt()
や .fromCodePoint()
との組み合わせが必要です(次章で紹介)。
まとめ
JavaScriptの文字列を扱うには、「見た目」と「内部構造」のズレを前提にする必要がある。
.length
や.charAt()
は信用しないfor...of
やArray.from()
を使う- 正規表現や
Intl.Segmenter
を活用すれば、正確な文字列処理ができる
👉 次章では、Unicode文字の**コードポイントの取得・変換方法(.codePointAt()
や .fromCodePoint()
)**にフォーカスします。
6. コードポイントの取得・変換方法
JavaScriptでは、文字のコードポイント(Unicodeの値)を取得・変換するために、次の2つのメソッドが用意されています。
String.prototype.codePointAt()
String.fromCodePoint()
これらを使うことで、サロゲートペアを意識せず、正確にUnicode文字を扱うことが可能になります。
codePointAt(index) – コードポイントの取得
基本構文
const code = "😄".codePointAt(0);
console.log(code); // 128516
console.log(code.toString(16)); // "1f604"
"😄"
は U+1F604(十進で 128516).charCodeAt()
とは異なり、サロゲートペアでも1文字として認識
注意点
const emoji = "😄";
console.log(emoji.charCodeAt(0)); // 0xD83D(上位サロゲート)
console.log(emoji.charCodeAt(1)); // 0xDE04(下位サロゲート)
console.log(emoji.codePointAt(0)); // 0x1F604(正しいコードポイント)
charCodeAt()
だとサロゲートが分かれてしまうのに対し、
codePointAt()
は 2ユニットを統合してコードポイントを返します。
String.fromCodePoint(cp) – コードポイントから文字を生成
基本構文
const emoji = String.fromCodePoint(0x1F604);
console.log(emoji); // 😄
- 補助面でも
\uD83D\uDE04
を意識せず、コードポイントのみで生成可能 - 通常の
String.fromCharCode()
ではサロゲートペアの処理ができない
複数の文字もOK
console.log(String.fromCodePoint(0x1F604, 0x2764)); // 😄❤
応用:文字列からコードポイント列へ → 再生成
const text = "🎉ありがとう";
const cps = [...text].map(ch => ch.codePointAt(0));
console.log(cps); // [127881, 12354, 12426, 12364, 12392, 12358]
const reconstructed = String.fromCodePoint(...cps);
console.log(reconstructed); // 🎉ありがとう
.charCodeAt() / fromCharCode() との違い
メソッド | 対応範囲 | 補助面(Emojiなど) |
---|---|---|
charCodeAt() |
UTF‑16コードユニット | ❌ サロゲート分離される |
codePointAt() |
Unicodeコードポイント | ✅ 正しく取得 |
fromCharCode() |
16bit単位(BMPまで) | ❌ 補助面不可 |
fromCodePoint() |
コードポイント対応 | ✅ 補助面もOK |
まとめ
- 補助面の文字(例:Emoji)を扱うなら、必ず
→
codePointAt()
/fromCodePoint()
を使う - サロゲートペアを意識せず、見た目どおりの文字単位で処理が可能
.charCodeAt()
やfromCharCode()
は基本的に使わない方が安全
7. ファイル・通信における文字エンコーディング
JavaScript内部では、すべての文字列がUTF-16のコードユニットの配列として扱われています(前章まで参照)。 しかし、実際の現場では――
- 📄 テキストファイルに保存する
- 🌐 ネットワーク経由で送信する
- 📦 APIレスポンスでJSONを受け取る
といった場面では、UTF-8 や その他の文字コードに変換されて扱われます。 つまり、JavaScriptの外側では「エンコーディングの選択」が不可欠になるのです。
JavaScriptの中と外の違い
観点 | JavaScript内部 | 通信・ファイル |
---|---|---|
表現 | UTF-16(コードユニット) | 主にUTF-8(バイト列) |
操作 | 文字列API (slice , codePointAt など) |
バイナリ変換が必要 (TextEncoder , Blob , Buffer ) |
目的 | ユーザー操作、描画 | 保存、転送、圧縮など |
文字列をUTF-8に変換(TextEncoder)
const text = "こんにちは";
const encoder = new TextEncoder(); // UTF-8 by default
const uint8 = encoder.encode(text);
console.log(uint8);
// → Uint8Array([...]) → バイト列としてのエンコード結果
TextEncoder
は UTF-8に変換(UTF-16 → UTF-8)- 通信(fetch, WebSocket)やファイル保存時に使用される形式
UTF-8 → 文字列へ(TextDecoder)
const decoder = new TextDecoder("utf-8");
const decoded = decoder.decode(uint8);
console.log(decoded); // こんにちは
- 受信した バイト列を再び文字列へ復元する
- 他にも
"shift_jis"
や"utf-16le"
を指定可能(対応ブラウザ確認要)
通信時の文字コード(fetchの例)
fetch("/data.json")
.then(res => res.text()) // UTF-8として読み込まれる
.then(data => console.log(data));
- HTMLやHTTPレスポンスの
Content-Type: application/json; charset=utf-8
が重要 - JavaScriptが自動でUTF-8として解釈する
ファイル保存・読み込み(Blob / FileReader)
// UTF-8でファイルダウンロード
const blob = new Blob(["こんにちは"], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "hello.txt";
a.click();
- Webアプリ上でUTF-8ファイルを生成・保存できる
- 逆に読み込む場合は
FileReader.readAsText(file, "utf-8")
を指定
エンコーディングミスが起きる場面
シーン | 問題例 |
---|---|
保存時の文字化け | UTF-16のまま保存 → 開いた時に文字化け |
通信先がSJIS | UTF-8で送信 → APIが対応せず「���」になる |
BOM有無の違い | UTF-8 with BOMとwithout BOMが混在して問題に |
※ 特に古いCSV処理やExcel連携ではShift_JISが絡みやすく要注意です。
ユースケース:日本語CSVの保存
const csv = "名前,年齢\n山田太郎,28";
const sjisBlob = new Blob([new TextEncoder("shift-jis").encode(csv)], {
type: "text/csv;charset=shift-jis"
});
// 上記は一部ブラウザで非対応 → 実用ではサーバー側でエンコード処理推奨
まとめ
- JavaScript内部は常に UTF-16
- 通信や保存では、主に UTF-8(ときに Shift_JIS や UTF-16)
TextEncoder
/TextDecoder
を使えば、安全に相互変換できる- ファイル形式・通信APIごとに適切なエンコーディング指定が必要
8. Unicode処理の実践Tipsと外部ライブラリ紹介
ここまで、JavaScriptにおけるUnicodeの内部構造、表記方法、そして安全な操作方法まで一通り解説してきました。 この最終章では、実際のプロジェクトやアプリ開発で役立つ実践Tipsと、Unicodeをより安全に扱える便利なライブラリをご紹介します。
よくあるUnicodeトラブルと回避策
トラブル | 原因 | 解決策 |
---|---|---|
絵文字が途中で切れる | サロゲートペアを分割 | Array.from() / for...of を使う |
文字数制限バグ | .length がコードユニット数 |
Intl.Segmenter または grapheme-splitter を使う |
正規表現でマッチしない | . , \w がBMP前提 |
u フラグ + \p{} 使用 |
ファイル保存で文字化け | UTF-8/Shift_JIS/BOMの違い | TextEncoder / Blob に明示的なエンコーディング指定 |
入力欄の「1文字制限」破壊 | 合成文字が複数ユニット | grapheme単位でカウント・制限する |
1. grapheme-splitter
- 「見た目の1文字(Grapheme Cluster)」単位で分割できる
- 合成文字やZJW文字にも対応
- 軽量・依存なし
import GraphemeSplitter from 'grapheme-splitter';
const splitter = new GraphemeSplitter();
const chars = splitter.splitGraphemes("👨👩👧👦こんにちは");
console.log(chars); // ["👨👩👧👦", "こ", "ん", "に", "ち", "は"]
2. xregexp
- 正規表現のUnicode拡張サポート
\p{Letter}
や\p{Han}
などをES6以前でも使用可能に- Unicodeカテゴリベースのフィルタリングに最適
3. unorm
- Unicode正規化(NFC/NFD/NFKC/NFKD)をサポート
- 結合文字と単体文字の比較処理に必須
"é".normalize() === "e\u0301".normalize(); // true
4. Intl.Segmenter
- グラフェム単位分割や単語・文単位の分割も可能
- 多言語対応・精度高
- モダンブラウザ対応(Polyfill未対応)
開発現場でのTips
フォーム入力のバリデーション
const input = "👩❤️👨";
const graphemes = [...input];
if (graphemes.length > 10) {
alert("入力は10文字以内にしてください");
}
データ通信での明示的encoding
fetch("/submit", {
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8"
},
body: JSON.stringify({ message: "こんにちは😄" })
});
ファイル保存時のBOM有無に注意
- Windows環境下のExcelで開くCSVには BOM付きUTF-8 が必要な場合あり
\uFEFF
を文字列の先頭に追加して出力する
const csv = "\uFEFF名前,年齢\n山田,28";
まとめ
- Unicodeは複雑ですが、理解とツールがあれば安全に扱えます
- JavaScriptの標準APIはだんだん進化しており、ES2015以降の機能を活用すればかなり安全
- 外部ライブラリと組み合わせることで、合成文字・絵文字・多言語対応も現実的に
💬 コメント