JavaScript | サロゲートペア

JavaScript JavaScript
スポンサーリンク

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" ← 真のコードポイント(👍)
JavaScript

4. 安全に「文字(コードポイント)単位」で扱う方法(初心者におすすめ)

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)); // '�' か 上位サロゲートだけ — 文字が壊れる
JavaScript

slice(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; // 見た目の文字数
JavaScript

6. よく使うユーティリティ関数(実務で役立つ)

文字(グラフ単位)配列にする

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
JavaScript

7. さらに注意:合成文字(グラフエム・クラスタ)と国旗など

  • 上の方法(Array.from / for...of)は サロゲートペア=単一コードポイント の扱いは解決しますが、さらに複雑な「人間が1つに見なす文字(グラフェムクラスタ)」があります。
    例:"👩‍👩‍👧‍👦"(ゼロ幅結合でつながれたファミリー絵文字)は複数のコードポイントからなる 1つの視覚的な文字 です。
  • これを厳密に扱うには グラフェムクラスタ単位 で分割する必要があり、Intl.Segmenter(一部環境)や外部ライブラリ(例:grapheme-splitter)を使うのが確実です。

簡単に言うと:

  • Array.fromfor...of でだいたいの絵文字やサロゲートは安全に扱える(初心者→実務でまず困らない)
  • ただし「複数コードポイントを結合して1つに見える特殊ケース」まで正確に扱うなら追加の手法が必要。

まとめ(初心者がまず押さえるべき3点)

  1. string.length16ビット単位の長さ → 絵文字が混ざると期待と違う。
  2. サロゲートペアを正しく扱うには for...of / [...str] / Array.from(str) を使う(これで大半の問題は解決)。
  3. もし「ユーザーが見た目上の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
タイトルとURLをコピーしました