やあ、調子はどうだい?
最近、チームのコードレビューをしていて「あー、またここでサロゲートペアの罠にハマってるな……」って膝を打つことが増えたんだよね。
UIの文字数制限をバリデーションしたり、ユーザー名やチャットの入力欄で絵文字をゴリゴリ処理したりする機能、よく実装するだろ? その時に `String.prototype.charAt()` や `length` プロパティを何の疑いもなく使ってしまい、本番環境で「絵文字を入れたら文字化けした」「変なところで文字列がぶった切られた」なんてバグを踏み抜く……これ、フロントエンドの現場あるあるなんだよね。
今回は、そんな文字エンコーディングの泥臭い闇から君を救い出すために、`String.prototype.codePointAt()` といういぶし銀のメソッドについて徹底的に解説しようと思う。実務で即戦力になる知識を詰め込んだから、最後までコーヒーでも飲みながら読んでいってくれ。
—
なぜ `charCodeAt()` ではダメなのか?(JavaScriptの歴史的負債)
まず大前提として、JavaScriptの文字列(String)は内部的に UTF-16 というエンコーディングで保持されている。これ、古の設計のせいで本当に厄介なんだ。
UTF-16では、基本的な文字(いわゆるBMP:基本多言語面)は16ビット(2バイト)のコードユニット1つで表現できる。だから昔は、文字の長さを測るのも `str.length` でよかったし、文字コードを取るのも `str.charCodeAt(index)` で十分だった。
しかし、世の中に絵文字(😀 や 🚀 など)やマニアックな漢字があふれるようになり、16ビット(65,536文字)だけでは世界中の文字を表しきれなくなった。そこで登場したのが、サロゲートペア(上位サロゲートと下位サロゲートのペア)だ。これらは2つのコードユニット(計4バイト)を組み合わせて1文字を表現する。
ここで、従来の `charCodeAt()` を使って絵文字を覗いてみるとどうなるか?
const emoji = ‘🚀’;
console.log(emoji.length); // 2 (おいおい、1文字なのに長さが2だ!)
console.log(emoji.charCodeAt(0)); // 55357 (0xD83D: 上位サロゲート)
console.log(emoji.charCodeAt(1)); // 56832 (0xDE80: 下位サロゲート)
見事に破綻しているだろ? `charCodeAt()` は「サロゲートペアの片割れ(16ビットの塊)」をそのまま返してしまうため、人間が認識している「1文字」の正しいUnicodeコードポイントが取得できないんだ。このままデータベースに突っ込んだり、文字数制限のバリデーションをかけたりすると、データが破損したり予期せぬバグの温床になったりする。
—
救世主 `String.prototype.codePointAt()` の仕様と内部挙動
そこで登場するのが、ES2015(ES6)で策定された `codePointAt()` だ。
こいつは、指定したインデックスにあるコードユニットからスタートし、後ろにサロゲートペアが続いているかを自動的に判定して、完全なUnicodeコードポイント(数値)を正しく復元して返してくれる優れものなんだよ。
ブラウザの裏側で何が起きているか?
1. インデックスの評価: 指定された `pos` の位置にあるUTF-16コードユニットを読み込む。
2. 上位サロゲートの判定: その値が `0xD800` から `0xDBFF` の範囲(上位サロゲート)かどうかチェックする。
3. ペアの結合: もし範囲内で、かつ次のインデックスに `0xDC00` から `0xDFFF` の範囲(下位サロゲート)が続いていれば、これら2つを数学的に結合して、本来の32ビットのUnicodeコードポイントを計算して返す。
4. 単体文字の場合: サロゲートペアの範囲外であれば、`charCodeAt()` と同様にそのコードユニットの値をそのまま返す。
言葉だけだと分かりにくいと思うので、実際のコードでその違いを確認してみよう。
—
実務で使える!キレイで堅牢なサンプルコード
現場のバリデーションや文字列処理でそのままコピペして使えるユーティリティ関数を用意した。エディタに貼り付けて動作を確認してみてほしい。
/
- 指定したインデックスの文字の正しいUnicodeコードポイントを取得し、
- ついでにその文字が何文字分のコードユニット(length)を消費しているかも返す実用的な関数
- @param {string} str – 対象の文字列
- @param {number} index – 調べたい位置(コードユニット単位のインデックス)
- @returns {Object} コードポイントと、それが占めるUTF-16の長さを返す
/
function getDetailedCharInfo(str, index) {
const codePoint = str.codePointAt(index);
if (codePoint === undefined) {
return null; // 範囲外アクセスの場合
}
// 10進数から16進数のUnicode表現(例: U+1F680)に変換
const hex = ‘U+’ + codePoint.toString(16).toUpperCase();
// 取得したコードポイントがBMP(基本多言語面)を超えているか(= サロゲートペアか)を判定
// 65535 (0xFFFF) より大きければ、サロゲートペアなので長さは 2
const charLength = codePoint > 0xFFFF ? 2 : 1;
return {
character: str.slice(index, index + charLength), // 正しい1文字を切り出す
codePoint: codePoint,
hexNotation: hex,
utf16Length: charLength
};
}
// — 検証してみよう —
const text = ‘A🚀B’;
console.log(getDetailedCharInfo(text, 0));
// 出力: { character: ‘A’, codePoint: 65, hexNotation: ‘U+41’, utf16Length: 1 }
console.log(getDetailedCharInfo(text, 1));
// 出力: { character: ‘🚀’, codePoint: 128640, hexNotation: ‘U+1F680’, utf16Length: 2 }
// ※注目: インデックス1を指定しても、codePointAtは自動で次のペアを読んで正しい絵文字を返してくれる!
console.log(getDetailedCharInfo(text, 3));
// 出力: { character: ‘B’, codePoint: 66, hexNotation: ‘U+42’, utf16Length: 1 }
// ※注意: ‘🚀’ が2文字分消費しているため、次の文字 ‘B’ のインデックスは 1 ではなく 3 になることに注意。
🚨 ここで現場のプロからの重要な注意点(Gotcha!)
上記のサンプル最後の「インデックスが 3 になる」という部分、すごく重要だから覚えておいてほしい。
`codePointAt(index)` の引数は、あくまで 「UTF-16のコードユニット単位のインデックス」 なんだ。文字の何番目(グリフ単位)かではない。
だから、以下のように単なる `for` ループで `i++` しながら回すと、サロゲートペアの「上位」と「下位」を二重に処理してしまったり、文字がズレたりするバグを生む。
// ❌ やりがちなアンチパターン(サロゲートペアを考慮できていない)
const str = ‘👨👩👦’; // 家族の絵文字(実は内部でZWJやサロゲートペアが複雑に絡み合っている超難敵)
for (let i = 0; i < str.length; i++) {
console.log(str.codePointAt(i)); // 途中で壊れたコードポイントを拾ってしまう!
}
現代のJavaScriptで文字列を「1文字ずつ(あるいは意味のある文字単位で)」安全にイテレートしたいときは、インデックスをゴリゴリ計算するよりも、ES2015の `for…of` ループを使うのがベストプラクティスだ。
// ✨ 推奨されるアプローチ(for…of は内部でサロゲートペアを正しく解釈してくれる)
const text = ‘A🚀B’;
for (const char of text) {
console.log(char, ‘CodePoint:’, char.codePointAt(0));
}
// 出力:
// A CodePoint: 65
// 🚀 CodePoint: 128640
// B CodePoint: 66
`for…of` なら、サロゲートペアを崩すことなく、人間が見ている「1文字」単位で安全にループを回せる。これ、テスト仕様書の文字数制限バリデーションとかを作る時に絶対に役立つから、脳みそに刻んでおいてほしい。
—
まとめ
- `charCodeAt()` は過去の遺物:サロゲートペア(絵文字など)を破壊するので、現代の開発では使わない。
- `codePointAt()` は正義:Unicodeの完全なコードポイントを安全に取得できる。
- インデックスの罠に注意:引数は文字単位ではなくコードユニット単位。安全に回したいなら `for…of` を使おう。
フロントエンドのコードは、ユーザーが画面で入力する「生の人間のデータ」を一番最初に受け止める最前線だ。文字コードの仕組みを理解しているかどうかで、プロダクトの品質(と、夜中に障害対応で呼ばれない平穏な睡眠時間)が大きく変わってくる。
今日の知見を次の機能開発からぜひ活かしてくれ。他にも分からないところがあったらいつでもデスクに聞きにきなよ。それじゃ、コーディングに戻るとしようか!

コメント