1. まず「コード単位」と「文字(コードポイント)」を区別する
- JavaScript の文字列は内部で UTF-16(16ビット単位=コード単位)で保持されています。
- ほとんどの文字(英字・数字・日本語の多く)は 1つの16ビット単位 で表せます(これを BMP = 基本多言語面 と呼びます)。
- しかし、絵文字や一部の特殊文字は 1文字につき 2 × 16ビット単位 を使います(これを サロゲートペア と呼ぶ)。
- 人間が「1文字」と考えるもの(絵文字や一部の合成文字)は、内部で 2個の要素(16ビット単位) に分かれていることがあります。
2. length が「文字数」と一致しないことがある
'abc'.length // 3
'あ'.length // 1
'👍'.length // 2 ← サロゲートペアのため length は 2 になる
JavaScript絵文字 👍 は「見た目は1文字」でも length は 2 を返す。見た目の「文字数」と length が違うことがある、これが多くのバグの原因です。
3. charCodeAt と codePointAt の違い
charCodeAt(i)は 16ビット単位の値(サロゲートの上位/下位部分)を返す。codePointAt(i)は Unicode のコードポイント(真の文字の番号) を返せる(サロゲートペアを正しく扱う)。
例:
const s = '👍'; // 見た目は1文字
s.charCodeAt(0).toString(16) // "d83d" ← 上位サロゲート
s.charCodeAt(1).toString(16) // "dc4d" ← 下位サロゲート
s.codePointAt(0).toString(16) // "1f44d" ← 真のコードポイント(👍)
JavaScript4. 安全に「文字(コードポイント)単位」で扱う方法(初心者におすすめ)
JavaScript で 見た目の1文字(ユーザーが想定する1文字) 単位で扱いたいとき、下の手法が便利です。
(A) for...of(文字を正しく分割してくれる)
for (const ch of 'a👍b') {
console.log(ch);
}
// 出力: 'a', '👍', 'b'
JavaScript(B) スプレッド演算子 / Array.from(配列にして扱う)
[...'a👍b'] // ['a', '👍', 'b']
Array.from('a👍b')// ['a', '👍', 'b']
JavaScriptこれらは UTF-16 のサロゲートペアを自動で合体させて「1つの文字」として分割してくれます。
5) よくあるバグ例と修正法(コードレビュー視点)
バグ例:先頭1文字だけ切り出したいのに壊れる
const name = '👍Halu';
console.log(name.slice(0, 1)); // '�' か 上位サロゲートだけ — 文字が壊れる
JavaScriptslice(0,1) は 16ビット単位 で切るので、サロゲートペアの片側だけを切り出してしまうことがある。
修正(安全な切り出し)
const safeFirst = [...name][0]; // '👍'
JavaScriptまたは
const safeSlice = (str, start, end) => [...str].slice(start, end).join('');
safeSlice('👍Halu', 0, 1); // '👍'
JavaScriptバグ例:文字数チェックで不正
ユーザー名の長さを if (name.length > 10) ... で判定していると、絵文字が混じると期待とずれる。
修正(見た目の文字数を使う)
const realLen = [...name].length; // 見た目の文字数
JavaScript6. よく使うユーティリティ関数(実務で役立つ)
文字(グラフ単位)配列にする
function toChars(str) {
return Array.from(str); // または [...str]
}
JavaScript見た目の文字数を返す
function visibleLength(str) {
return [...str].length;
}
JavaScript安全な substring / slice
function safeSubstring(str, start, end) {
return [...str].slice(start, end).join('');
}
JavaScript文字ごとに codePoint を見る(デバッグ用)
for (const ch of 'a👍b') {
console.log(ch, ch.codePointAt(0).toString(16));
}
// a 61
// 👍 1f44d
// b 62
JavaScript7. さらに注意:合成文字(グラフエム・クラスタ)と国旗など
- 上の方法(
Array.from/for...of)は サロゲートペア=単一コードポイント の扱いは解決しますが、さらに複雑な「人間が1つに見なす文字(グラフェムクラスタ)」があります。
例:"👩👩👧👦"(ゼロ幅結合でつながれたファミリー絵文字)は複数のコードポイントからなる 1つの視覚的な文字 です。 - これを厳密に扱うには グラフェムクラスタ単位 で分割する必要があり、
Intl.Segmenter(一部環境)や外部ライブラリ(例:grapheme-splitter)を使うのが確実です。
簡単に言うと:
Array.fromやfor...ofでだいたいの絵文字やサロゲートは安全に扱える(初心者→実務でまず困らない)- ただし「複数コードポイントを結合して1つに見える特殊ケース」まで正確に扱うなら追加の手法が必要。
まとめ(初心者がまず押さえるべき3点)
string.lengthは 16ビット単位の長さ → 絵文字が混ざると期待と違う。- サロゲートペアを正しく扱うには
for...of/[...str]/Array.from(str)を使う(これで大半の問題は解決)。 - もし「ユーザーが見た目上の1文字」を厳密に扱う必要があるなら
Intl.Segmenterや専門ライブラリを検討する。
すぐ使える例(コピーして使えるスニペット)
// 見た目の長さ(文字数)を取得
function visibleLength(str) {
return [...str].length;
}
// 安全に先頭n文字を切り出す
function safeSlice(str, n) {
return [...str].slice(0, n).join('');
}
// 例
console.log(visibleLength('abc')); // 3
console.log(visibleLength('👍abc')); // 4
console.log(safeSlice('👍abc', 1)); // '👍'
JavaScript
